Day17_用户认证 Zuul
1 用户认证
1.1 用户认证流程分析
用户认证流程如下:
业务流程说明如下:
1、客户端请求认证服务进行认证。
2、认证服务认证通过向浏览器cookie写入token(身份令牌)
认证服务请求用户中心查询用户信息。
认证服务请求Spring Security申请令牌。
认证服务将token(身份令牌)和jwt令牌存储至redis中。
认证服务向cookie写入 token(身份令牌)。
3、前端携带token请求认证服务获取jwt令牌
前端获取到jwt令牌并存储在sessionStorage。
前端从jwt令牌中解析中用户信息并显示在页面。
4、前端携带cookie中的token身份令牌及jwt令牌访问资源服务
前端请求资源服务需要携带两个token,一个是cookie中的身份令牌,一个是http header中的jwt令牌
前端请求资源服务前在http header上添加jwt请求资源
5、网关校验token的合法性
用户请求必须携带token身份令牌和jwt令牌
网关校验redis中token是否合法,已过期则要求用户重新登录
6、资源服务校验jwt的合法性并完成授权
资源服务校验jwt令牌,完成授权,拥有权限的方法正常执行,没有权限的方法将拒绝访问。
1.2 认证服务查询数据库
1.2.1 需求分析
认证服务根据数据库中的用户信息去校验用户的身份,即校验账号和密码是否匹配。
认证服务不直接连接数据库,而是通过用户中心服务去查询用户中心数据库。
完整的流程图如下:
1.2.2 搭建环境
1.2.2.1 创建用户中心数据库
用户中心负责用户管理,包括:用户信息管理、角色管理、权限管理等。
创建xc_user数据库(MySQL)
导入xc_user.sql(已导入不用重复导入)
1.2.2.2 创建用户中心工程
导入“资料”中的xc-service-ucenter
1.2.3 查询用户接口
完成用户中心根据账号查询用户信息接口功能。
1.2.3.1 Api接口
用户中心对外提供如下接口:
1、响应数据类型
此接口将来被用来查询用户信息及用户权限信息,所以这里定义扩展类型。
package com.xuecheng.framework.domain.ucenter.ext;
import com.xuecheng.framework.domain.ucenter.XcMenu;
import com.xuecheng.framework.domain.ucenter.XcUser;
import lombok.Data;
import lombok.ToString;
import java.util.List;
/**
* Created by admin on 2018/3/20.
*/
@Data
@ToString
public class XcUserExt extends XcUser {
//权限信息
private List<XcMenu> permissions;
//企业信息
private String companyId;
}
2、根据账号查询用户信息
package com.xuecheng.api.ucenter;
import com.xuecheng.framework.domain.ucenter.ext.XcUserExt;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
/**
* Created by Administrator.
*/
@Api(value = "用户中心",description = "用户中心管理")
public interface UcenterControllerApi {
@ApiOperation("根据用户账号查询用户信息")
public XcUserExt getUserext(String username);
}
1.2.3.2 DAO
添加XcUser、XcCompantUser两个表的Dao。
package com.xuecheng.ucenter.dao;
import com.xuecheng.framework.domain.ucenter.XcCompanyUser;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* Created by Administrator.
*/
public interface XcCompanyUserRepository extends JpaRepository<XcCompanyUser,String> {
//根据用户id查询该用户所属的公司id
XcCompanyUser findByUserId(String userId);
}
package com.xuecheng.ucenter.dao;
import com.xuecheng.framework.domain.ucenter.XcUser;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* Created by Administrator.
*/
public interface XcUserRepository extends JpaRepository<XcUser,String> {
//根据账号查询用户信息
XcUser findByUsername(String username);
}
1.2.3.3 Service
package com.xuecheng.ucenter.service;
import com.xuecheng.framework.domain.ucenter.XcCompanyUser;
import com.xuecheng.framework.domain.ucenter.XcUser;
import com.xuecheng.framework.domain.ucenter.ext.XcUserExt;
import com.xuecheng.ucenter.dao.XcCompanyUserRepository;
import com.xuecheng.ucenter.dao.XcUserRepository;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author Administrator
* @version 1.0
**/
@Service
public class UserService {
@Autowired
XcUserRepository xcUserRepository;
@Autowired
XcCompanyUserRepository xcCompanyUserRepository;
//根据账号查询xcUser信息
public XcUser findXcUserByUsername(String username){
return xcUserRepository.findByUsername(username);
}
//根据账号查询用户信息
public XcUserExt getUserExt(String username){
//根据账号查询xcUser信息
XcUser xcUser = this.findXcUserByUsername(username);
if(xcUser == null){
return null;
}
//用户id
String userId = xcUser.getId();
//根据用户id查询用户所属公司id
XcCompanyUser xcCompanyUser = xcCompanyUserRepository.findByUserId(userId);
//取到用户的公司id
String companyId = null;
if(xcCompanyUser!=null){
companyId = xcCompanyUser.getCompanyId();
}
XcUserExt xcUserExt = new XcUserExt();
BeanUtils.copyProperties(xcUser,xcUserExt);
xcUserExt.setCompanyId(companyId);
return xcUserExt;
}
}
1.2.3.4 Controller
package com.xuecheng.ucenter.controller;
import com.xuecheng.api.ucenter.UcenterControllerApi;
import com.xuecheng.framework.domain.ucenter.ext.XcUserExt;
import com.xuecheng.ucenter.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Administrator
* @version 1.0
**/
@RestController
@RequestMapping("/ucenter")
public class UcenterController implements UcenterControllerApi {
@Autowired
UserService userService;
@Override
@GetMapping("/getuserext")
public XcUserExt getUserext(@RequestParam("username") String username) {
return userService.getUserExt(username);
}
}
1.2.4.4 测试
使用Swagger-ui或postman测试用户信息查询接口。
http://127.0.0.1:40300/ucenter/getuserext?username=itcast
1.2.5 调用查询用户接口
1.2.5.1 创建client
认证服务需要远程调用用户中心服务查询用户,在认证服务中创建Feign客户端
package com.xuecheng.auth.client;
import com.xuecheng.framework.client.XcServiceList;
import com.xuecheng.framework.domain.ucenter.ext.XcUserExt;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* Created by Administrator.
*/
@FeignClient(value = XcServiceList.XC_SERVICE_UCENTER)
public interface UserClient {
//根据账号查询用户信息
@GetMapping("/ucenter/getuserext")
public XcUserExt getUserext(@RequestParam("username") String username);
}
1.2.5.2 UserDetailsServiceImpl
认证服务调用spring security接口申请令牌,spring security接口会调用UserDetailsServiceImpl从数据库查询用户,如果查询不到则返回 NULL,表示不存在;在UserDetailsServiceImpl中将正确的密码返回, spring security 会自动去比对输入密码的正确性。
1、修改UserDetailsServiceImpl的loadUserByUsername方法,调用Ucenter服务的查询用户接口
package com.xuecheng.auth.service;
import com.xuecheng.auth.client.UserClient;
import com.xuecheng.framework.domain.ucenter.XcMenu;
import com.xuecheng.framework.domain.ucenter.ext.XcUserExt;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserClient userClient;
@Autowired
ClientDetailsService clientDetailsService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//取出身份,如果身份为空说明没有认证
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//没有认证统一采用httpbasic认证,httpbasic中存储了client_id和client_secret,开始认证client_id和client_secret
if(authentication==null){
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username);
if(clientDetails!=null){
//密码
String clientSecret = clientDetails.getClientSecret();
return new User(username,clientSecret,AuthorityUtils.commaSeparatedStringToAuthorityList(""));
}
}
if (StringUtils.isEmpty(username)) {
return null;
}
//远程调用用户中心根据账号查询用户信息
XcUserExt userext = userClient.getUserext(username);
if(userext == null){
//返回空给spring security表示用户不存在
return null;
}
// XcUserExt userext = new XcUserExt();
// userext.setUsername("itcast");
// userext.setPassword(new BCryptPasswordEncoder().encode("123"));
userext.setPermissions(new ArrayList<XcMenu>());//权限暂时用静态的
//取出正确密码(hash值)
String password = userext.getPassword();
//这里暂时使用静态密码
// String password ="123";
//用户权限,这里暂时使用静态数据,最终会从数据库读取
//从数据库获取权限
List<XcMenu> permissions = userext.getPermissions();
List<String> user_permission = new ArrayList<>();
permissions.forEach(item-> user_permission.add(item.getCode()));
// user_permission.add("course_get_baseinfo");
// user_permission.add("course_find_pic");
String user_permission_string = StringUtils.join(user_permission.toArray(), ",");
UserJwt userDetails = new UserJwt(username,
password,
AuthorityUtils.commaSeparatedStringToAuthorityList(user_permission_string));
userDetails.setId(userext.getId());
userDetails.setUtype(userext.getUtype());//用户类型
userDetails.setCompanyId(userext.getCompanyId());//所属企业
userDetails.setName(userext.getName());//用户名称
userDetails.setUserpic(userext.getUserpic());//用户头像
/* UserDetails userDetails = new org.springframework.security.core.userdetails.User(username,
password,
AuthorityUtils.commaSeparatedStringToAuthorityList(""));*/
// AuthorityUtils.createAuthorityList("course_get_baseinfo","course_get_list"));
return userDetails;
}
}
2、测试,请求http://localhost:40400/auth/userlogin
观察UserDetailsServiceImpl是否正常请求Ucenter的查询用户接口。
1.2.5.3 BCryptPasswordEncoder
早期使用md5对密码进行编码,每次算出的md5值都一样,这样非常不安全,Spring Security推荐使用 BCryptPasswordEncoder对密码加随机盐,每次的Hash值都不一样,安全性高。
1、BCryptPasswordEncoder测试程序如下
package com.xuecheng.ucenter.test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author HackerStar
* @create 2020-09-09 20:11
*/
public class Test {
@org.junit.Test
public void testPasswrodEncoder() {
String password = "111111";
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
for (int i = 0; i < 10; i++) {
//每个计算出的Hash值都不一样
String hashPass = passwordEncoder.encode(password);
System.out.println(hashPass); //虽然每次计算的密码Hash值不一样但是校验是通过的
boolean f = passwordEncoder.matches(password, hashPass);
System.out.println(f);
}
}
}
2、在AuthorizationServerConfig配置类中配置BCryptPasswordEncoder
代码中已经配置好了。
3、测试
请求http://localhost:40400/auth/userlogin,输入正常的账号和密码进行测试。
1.2.5.4 解析申请令牌错误信息
当账号输入错误应该返回用户不存在的信息,当密码错误要返回用户名或密码错误信息,业务流程图如下:
修改申请令牌的程序解析返回的错误(AuthService):
由于restTemplate收到400或401的错误会抛出异常,而spring security针对账号不存在及密码错误会返回400及401,所以在代码中控制针对400或401的响应不要抛出异常。
package com.xuecheng.auth.service;
import com.alibaba.fastjson.JSON;
import com.xuecheng.framework.client.XcServiceList;
import com.xuecheng.framework.domain.ucenter.ext.AuthToken;
import com.xuecheng.framework.domain.ucenter.response.AuthCode;
import com.xuecheng.framework.exception.ExceptionCast;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Service;
import org.springframework.util.Base64Utils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author Administrator
* @version 1.0
**/
@Service
public class AuthService {
@Value("${auth.tokenValiditySeconds}")
int tokenValiditySeconds;
@Autowired
LoadBalancerClient loadBalancerClient;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
RestTemplate restTemplate;
//用户认证申请令牌,将令牌存储到redis
public AuthToken login(String username, String password, String clientId, String clientSecret) {
//请求spring security申请令牌
AuthToken authToken = this.applyToken(username, password, clientId, clientSecret);
if(authToken == null){
ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLYTOKEN_FAIL);
}
//用户身份令牌
String access_token = authToken.getAccess_token();
//存储到redis中的内容
String jsonString = JSON.toJSONString(authToken);
//将令牌存储到redis
boolean result = this.saveToken(access_token, jsonString, tokenValiditySeconds);
if (!result) {
ExceptionCast.cast(AuthCode.AUTH_LOGIN_TOKEN_SAVEFAIL);
}
return authToken;
}
//存储到令牌到redis
/**
*
* @param access_token 用户身份令牌
* @param content 内容就是AuthToken对象的内容
* @param ttl 过期时间
* @return
*/
private boolean saveToken(String access_token,String content,long ttl){
String key = "user_token:" + access_token;
stringRedisTemplate.boundValueOps(key).set(content,ttl, TimeUnit.SECONDS);
Long expire = stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
return expire>0;
}
//删除token
public boolean delToken(String access_token){
String key = "user_token:" + access_token;
stringRedisTemplate.delete(key);
return true;
}
//从redis查询令牌
public AuthToken getUserToken(String token){
String key = "user_token:" + token;
//从redis中取到令牌信息
String value = stringRedisTemplate.opsForValue().get(key);
//转成对象
try {
AuthToken authToken = JSON.parseObject(value, AuthToken.class);
return authToken;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
//申请令牌
private AuthToken applyToken(String username, String password, String clientId, String clientSecret){
//从eureka中获取认证服务的地址(因为spring security在认证服务中)
//从eureka中获取认证服务的一个实例的地址
ServiceInstance serviceInstance = loadBalancerClient.choose(XcServiceList.XC_SERVICE_UCENTER_AUTH);
//此地址就是http://ip:port
URI uri = serviceInstance.getUri();
//令牌申请的地址 http://localhost:40400/auth/oauth/token
String authUrl = uri+ "/auth/oauth/token";
//定义header
LinkedMultiValueMap<String, String> header = new LinkedMultiValueMap<>();
String httpBasic = getHttpBasic(clientId, clientSecret);
header.add("Authorization",httpBasic);
//定义body
LinkedMultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type","password");
body.add("username",username);
body.add("password",password);
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(body, header);
//String url, HttpMethod method, @Nullable HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables
//设置restTemplate远程调用时候,对400和401不让报错,正确返回数据
restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
@Override
public void handleError(ClientHttpResponse response) throws IOException {
if(response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401){
super.handleError(response);
}
}
});
ResponseEntity<Map> exchange = restTemplate.exchange(authUrl, HttpMethod.POST, httpEntity, Map.class);
//申请令牌信息
Map bodyMap = exchange.getBody();
if(bodyMap == null ||
bodyMap.get("access_token") == null ||
bodyMap.get("refresh_token") == null ||
bodyMap.get("jti") == null){
//解析spring security返回的错误信息
if(bodyMap!=null && bodyMap.get("error_description")!=null){
String error_description = (String) bodyMap.get("error_description");
if(error_description.indexOf("UserDetailsService returned null")>=0){
ExceptionCast.cast(AuthCode.AUTH_ACCOUNT_NOTEXISTS);
}else if(error_description.indexOf("坏的凭证")>=0){
ExceptionCast.cast(AuthCode.AUTH_CREDENTIAL_ERROR);
}
}
return null;
}
AuthToken authToken = new AuthToken();
authToken.setAccess_token((String) bodyMap.get("jti"));//用户身份令牌
authToken.setRefresh_token((String) bodyMap.get("refresh_token"));//刷新令牌
authToken.setJwt_token((String) bodyMap.get("access_token"));//jwt令牌
return authToken;
}
//获取httpbasic的串
private String getHttpBasic(String clientId,String clientSecret){
String string = clientId+":"+clientSecret;
//将串进行base64编码
byte[] encode = Base64Utils.encode(string.getBytes());
return "Basic "+new String(encode);
}
}
用户不存在:
密码错误:
1.2.5.5 测试
使用postman请求http://localhost:40400/auth/userlogin
1、输入正确的账号和密码进行测试
从数据库找到测试账号,本课程所提供的用户信息初始密码统一为111111
2、输入错误的账号和密码进行测试
1.3 用户登录前端
1.3.1 需求分析
点击用户登录固定跳转到用户中心前端的登录页面,如下:
输入账号和密码,登录成功,跳转到首页。
用户中心前端(xc-ui-pc-learning工程)提供登录页面,所有子系统连接到此页面。
说明:
页面有“登录|注册”链接的前端系统有:门户系统、搜索系统、用户中心。
本小节修改门户系统的页头,其它三处可参考门户修改。
/*登陆*/
export const login = params => {
//let loginRequest = querystring.stringify(params)
let loginRequest = qs.stringify(params);
return http.requestPostForm('/openapi/auth/userlogin',loginRequest);
}
1.3.2 页面
1、登录页面
进入用户中心前端,找到登录页面loginpage.vue:
loginpage.vue使用了loginForm.vue组件,loginForm.vue页面包括了登录表单。
2、路由配置
在home模块配置路由:
3、登录后跳转
请求登录页面需携带returnUrl参数,要求此参数使用Base64编码。
登录成功后将跳转到returnUrl,loginForm.vue组件的登录方法如下:
login: function () {
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.editLoading = true;
let para = Object.assign({}, this.loginForm);
loginApi.login(para).then((res) => {
this.editLoading = false;
if(res.success){
this.$message('登陆成功');
//刷新 当前页面
// alert(this.returnUrl)
console.log(this.returnUrl)
if(this.returnUrl!='undefined' && this.returnUrl!=''
&& !this.returnUrl.includes("/userlogout")
&& !this.returnUrl.includes("/userlogin")){
window.location.href = this.returnUrl;
}else{
//跳转到首页
window.location.href = 'http://www.xuecheng.com/'
}
}else{
if(res.message){
this.$message.error(res.message);
}else{
this.$message.error('登陆失败');
}
}
},
(res) => {
this.editLoading = false;
});
}
});
},
1.3.3 点击登录页面
在门户的页头点击“登录|注册”连接到用户中心的登录页面,并且携带returnUrl。
修改门户的header.vue(代码已经写好了),代码如下:
......
<a href="javascript:;" @click="showlogin" v-if="logined == false">登陆|注册</a>
......
showlogin:function(){
this.returnUrl = window.location;
this.LoginFormVisible = true;
}
......
1.3.4 测试
测试之前修改认证服务的配置:
修改 application.yml中cookie域名
cookieDomain: xuecheng.com
测试流程如下:
1、输入www.xuecheng.com进入系统(需要在hosts文件配置)
2、输入正确的账号和密码,提交
3、输入错误的账号和密码,提交
登录成功,观察cookie是否存储成功:
2 前端显示当前用户
2.1 需求分析
用户登录成功在页头显示当前登录的用户名称。
数据流程如下图:
1、用户请求认证服务,登录成功。
2、用户登录成功,认证服务向cookie写入身份令牌,向redis写入user_token(身份令牌及授权jwt授权令牌)。
3、客户端携带cookie中的身份令牌请求认证服务获取jwt令牌。
4、客户端解析jwt令牌,并将解析的用户信息存储到sessionStorage中。
jwt令牌中包括了用户的基本信息,客户端解析jwt令牌即可获取用户信息。
5、客户端从sessionStorage中读取用户信息,并在页头显示。
sessionStorage :
sessionStorage 是H5的一个会话存储对象,在SessionStorage中保存的数据只在同一窗口或同一标签页中有效, 在关闭窗口之后将会删除SessionStorage中的数据。
seesionStorage的存储方式采用key/value的方式,可保存5M左右的数据(不同的浏览器会有区别)。
2.2 jwt 查询接口
2.2.1 需求分析
认证服务对外提供jwt查询接口,流程如下:
1、客户端携带cookie中的身份令牌请求认证服务获取jwt
2、认证服务根据身份令牌从redis中查询jwt令牌并返回给客户端。
2.2.2 API
在认证模块定义 jwt查询接口:
package com.xuecheng.api.auth;
import com.xuecheng.framework.domain.ucenter.request.LoginRequest;
import com.xuecheng.framework.domain.ucenter.response.JwtResult;
import com.xuecheng.framework.domain.ucenter.response.LoginResult;
import com.xuecheng.framework.model.response.ResponseResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
/**
* Created by Administrator.
*/
@Api(value = "用户认证", description = "用户认证接口")
public interface AuthControllerApi {
@ApiOperation("登录")
public LoginResult login(LoginRequest loginRequest);
@ApiOperation("退出")
public ResponseResult logout();
@ApiOperation("查询userjwt令牌")
public JwtResult userjwt();
}
2.2.3 Dao
无
2.2.4 Service
在AuthService中定义方法如下:
//从redis查询令牌
public AuthToken getUserToken(String token) {
String key = "user_token:" + token;
//从redis中取到令牌信息
String value = stringRedisTemplate.opsForValue().get(key);
//转成对象
try {
AuthToken authToken = JSON.parseObject(value, AuthToken.class);
return authToken;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
2.2.5 Controller
@Override
@GetMapping("/userjwt")
public JwtResult userjwt() {
//获取cookie中的令牌
String access_token = getTokenFormCookie();
//根据令牌从redis查询jwt
AuthToken authToken = authService.getUserToken(access_token);
if (authToken == null) {
return new JwtResult(CommonCode.FAIL, null);
}
return new JwtResult(CommonCode.SUCCESS, authToken.getJwt_token());
}
//从cookie中读取访问令牌
private String getTokenFormCookie() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
Map<String, String> cookieMap = CookieUtil.readCookie(request, "uid");
String access_token = cookieMap.get("uid");
return access_token;
}
2.2.6 测试
使用postman测试
1、请求 /auth/userlogin
观察cookie是否已存入用户身份令牌。
2、get请求jwt
2.3 前端请求jwt
2.3.1 需求分析
前端需求如下:
用户登录成功,前端请求认证服务获取jwt令牌。
前端解析jwt令牌的内容,得到用户信息,并将用户信息存储到sessionStorage。
从 sessionStorage取出用户信息在页头显示用户名称。
2.3.2 API方法
在login.js中定义getjwt方法:
/*获取jwt令牌*/
const getjwt = () => {
return requestGet('/openapi/auth/userjwt');
}
2.3.3 页面
修改include/header.html
<span v-if="logined == true">欢迎{{this.user.username}}</span>
<a href="javascript:;" @click="logout" v-if="logined == true">退出</a>
<a href="http://ucenter.xuecheng.com/" class="personal" target="_blank">我的学习</a>
<a href="javascript:;" @click="showlogin" v-if="logined == false">登陆 | 注册</a>
<a href="http://teacher.xuecheng.com/" class="personal" target="_blank">教学提供方</a>
<a href="http://system.xuecheng.com/" class="personal" target="_blank">系统后台</a>
用户登录成功设置数据对象logined为true,设置数据对象user为当前用户信息。
数据对象定义如下:
data: {
keyword: '',
loginFormVisible: false,
activeName: 'login',
editLoading: false,
formLabelWidth: '120px',
user:{
userid:'',
username: '',
userpic: ''
},
logined:false
},
2、解析jwt令牌
在util.js中定义解析jwt令牌方法:
//解析jwt令牌,获取用户信息
getUserInfoFromJwt : function (jwt) {
if(!jwt){
return ;
}
var jwtDecodeVal = jwtDecode(jwt);
if (!jwtDecodeVal) {
return ;
}
let activeUser={}
//console.log(jwtDecodeVal)
activeUser.utype = jwtDecodeVal.utype || '';
activeUser.username = jwtDecodeVal.name || '';
activeUser.userpic = jwtDecodeVal.userpic || '';
activeUser.userid = jwtDecodeVal.userid || '';
activeUser.authorities = jwtDecodeVal.authorities || '';
activeUser.uid = jwtDecodeVal.jti || '';
activeUser.jwt = jwt;
return activeUser;
},
3、refresh_user()
在mounted钩子方法中获取当前用户信息,并将用户信息存储到sessionStorage
mounted() {
this.refresh_user()
}
refresh_user()方法如下:
refresh_user:function(){
//从sessionStorage中取出当前用户
let activeUser= getActiveUser();
//取出cookie中的令牌
let uid = getCookie("uid")
//console.log(activeUser)
if(activeUser && uid && uid == activeUser.uid){
this.logined = true
this.user = activeUser;
}else{
if(!uid){
return ;
}
//请求查询jwt
getjwt().then((res) => {
if(res.success){
let jwt = res.jwt;
let activeUser = getUserInfoFromJwt(jwt)
if(activeUser){
this.logined = true
this.user = activeUser;
setUserSession("activeUser",JSON.stringify(activeUser))
}
}
})
}
},
2.3.4 配置代理转发
上边实现在首页显示当前用户信息,首页需要通过Nginx代理请求认证服务,所以需要在首页(www.xuecheng.com)的虚拟主机上配置代理路径:
#认证
location ^~ /openapi/auth/ {
proxy_pass http://auth_server_pool/auth/;
}
之前已经配置过了。
注意:其它前端系统要接入认证要请求认证服务也需要配置上边的代理路径。
3 用户退出
3.1 需求分析
操作流程如下:
1、用户点击退出,弹出退出确认窗口,点击确定
2、退出成功
用户退出要以下动作:
1、删除redis中的token
2、删除cookie中的token
3.2 API
认证服务对外提供退出接口。
package com.xuecheng.api.auth;
import com.xuecheng.framework.domain.ucenter.request.LoginRequest;
import com.xuecheng.framework.domain.ucenter.response.JwtResult;
import com.xuecheng.framework.domain.ucenter.response.LoginResult;
import com.xuecheng.framework.model.response.ResponseResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
/**
* Created by Administrator.
*/
@Api(value = "用户认证", description = "用户认证接口")
public interface AuthControllerApi {
@ApiOperation("登录")
public LoginResult login(LoginRequest loginRequest);
@ApiOperation("退出")
public ResponseResult logout();
@ApiOperation("查询userjwt令牌")
public JwtResult userjwt();
}
3.3 服务端
认证服务提供退出接口。
3.3.1 DAO
无。
3.3.2 Service
//删除token
public boolean delToken(String access_token) {
String key = "user_token:" + access_token;
stringRedisTemplate.delete(key);
return true;
}
方法已经定义过了。
3.3.3 Controller
//退出
@Override
@PostMapping("/userlogout")
public ResponseResult logout() {
//取出身份令牌
String uid = getTokenFormCookie();
//删除redis中token
authService.delToken(uid);
//清除cookie
clearCookie(uid);
return new ResponseResult(CommonCode.SUCCESS);
}
//清除cookie
private void clearCookie(String token) {
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
CookieUtil.addCookie(response, cookieDomain, "/", "uid", token, 0, false);
}
3.3.4 退出URL放行
认证服务默认都要校验用户的身份信息,这里需要将退出url放行。
在WebSecurityConfig类中重写 configure(WebSecurity web)方法,如下:
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/userlogin","/userlogout","/userjwt");
}
3.4 前端
3.4.1 需求分析
在用户中心前端工程(xc-ui-pc-learning)开发退出页面。
3.4.2 Api方法
在用户中心工程增加退出的api方法
在base模块的login.js增加方法如下:
/*退出*/
export const logout = params => {
return http.requestPost('/openapi/auth/userlogout');
}
3.4.3 退出页面
1、在用户中心工程创建退出页面
2、路由配置
import Home from '@/module/home/page/home.vue';
import Login from '@/module/home/page/loginpage.vue';
import Denied from '@/module/home/page/denied.vue';
import Logout from '@/module/home/page/logout.vue';
import order_pay from '@/module/order/page/order_pay.vue';
// import LoginMini from '@/module/home/page/login_mini.vue';
export default [{
path: '/',
component: Home,
name: '个人中心',
hidden: true
},
{
path: '/login',
component: Login,
name: 'Login',
hidden: true
},
{
path: '/logout',
component: Logout,
name: 'Logout',
hidden: true
},
{
path: '/denied',
component: Denied,
name: Denied,
hidden: true
}/*,
{
path: '/loginmini',
component: LoginMini,
name: 'LoginMini',
hidden: true
}*/
]
3、退出方法
退出成功清除页面的sessionStorage
参考logout.vue
在created钩子方法请求退出方法
created(){
loginApi.logout({}).then((res) => {
if(res.success){
sessionStorage.removeItem('activeUser');
this.$message('退出成功');
this.logoutsuccess = true
}else{
this.logoutsuccess = false
}
},
(res) => {
this.logoutsuccess = false
});
},
3.4.4 连接到退出页面
修改include/header.html
<a href="javascript:;" @click="logout" v-if="logined == true">退出</a>
在include/header.html中添加element-ui库:
<script src="/css/el/index.js"></script>
将此js加到head的最下边
logout方法如下:
logout: function () {
this.$confirm('确认退出吗?', '提示', {
}).then(() => {
//跳转到统一登陆
window.location = "http://ucenter.xuecheng.com/#/logout"
}).catch(() => {
});
},
3.4.5 测试
1、用户登录成功
2、点击退出
4 Zuul网关
4.1 需求分析
网关的作用相当于一个过滤器、拦截器,它可以拦截多个系统的请求。
本章节要使用网关校验用户的身份是否合法。
4.2 Zuul 介绍
什么是Zuul?
Spring Cloud Zuul是整合Netflix公司的Zuul开源项目实现的微服务网关,它实现了请求路由、负载均衡、校验过滤等功能。
官方:https://github.com/Netflix/zuul
什么是网关?
服务网关是在微服务前边设置一道屏障,请求先到服务网关,网关会对请求进行过滤、校验、路由等处理。有了服务网关可以提高微服务的安全性,网关校验请求的合法性,请求不合法将被拦截,拒绝访问。
4.3 搭建网关工程
创建网关工程(xc-govern-gateway):
1、创建xc-govern-gateway工程
导入“资料”中的xc-govern-gateway。
2、@EnableZuulProxy
注意在启动类上使用@EnableZuulProxy注解标识此工程为Zuul网关,启动类代码如下:
package com.xuecheng.govern.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
/**
* @author Administrator
* @version 1.0
* @create 2018-07-17 12:03
**/
@SpringBootApplication
@EnableZuulProxy//此工程是一个zuul网关
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
4.4 路由配置
4.4.1 需求分析
Zuul网关具有代理的功能,根据请求的url转发到微服务,如下图:
客户端请求网关/api/learning,通过路由转发到/learning。
客户端请求网关/api/course,通过路由转发到/course。
4.4.2 路由配置
在appcation.yml中配置:
zuul:
routes:
manage-course:
path: /course/**
serviceId: xc-service-manage-course #微服务名称,网关会从eureka中获取该服务名称下的服务实例的地址
# 例子:将请求转发到http://localhost:31200/course
#url: http://www.baidu.com #也可指定url,此url也可以是外网地址\
strip-prefix: false #true:代理转发时去掉前缀,false:代理转发时不去掉前缀
sensitiveHeaders: #默认zuul会屏蔽cookie,cookie不会传到下游服务,这里设置为空则取消默认的黑名单,如果设置了具体的头信息则不会传到下游服务
# ignoredHeaders: 默认为空表示不过虑任何头
serviceId:推荐使用serviceId,zuul会从Eureka中找到服务id对应的ip和端口。
strip-prefix: false
true:代理转发时去掉前缀,false:代理转发时不去掉前缀,例如,为true请求/course/coursebase/get/..,代理转发到/coursebase/get/,如果为false则代理转发到/course/coursebase/get
sensitiveHeaders:敏感头设置,默认会过滤掉cookie,这里设置为空表示不过滤
ignoredHeaders:可以设置过滤的头信息,默认为空表示不过滤任何头
4.4.3 测试
请求http://localhost:50201/api/course/coursepic/list/4028e58161bd22e60161bd23672a0001查询课程图片信息。
http://localhost:50201/api是网关地址,通过路由转发到xc-service-manage-course服务。
由于课程管理已经添加了授课拦截,这里为了测试网关功能暂时将“/course/coursepic/list”url排除认证。
在课程管理服务的 ResourceServerConfig类中添加"/course/coursepic/list/*",代码如下:
//Http安全配置,对每个到达系统的http请求链接进行校验
@Override
public void configure(HttpSecurity http) throws Exception {
//所有请求必须认证通过
http.authorizeRequests()
//下边的路径放行
.antMatchers("/v2/api-docs", "/swagger-resources/configuration/ui",
"/swagger-resources","/swagger-resources/configuration/security",
"/swagger-ui.html","/webjars/**", "/course/coursepic/list/*").permitAll()
.anyRequest().authenticated();
}
4.4.4 完整的路由配置
zuul:
routes:
manage-course:
path: /course/**
serviceId: xc-service-manage-course #微服务名称,网关会从eureka中获取该服务名称下的服务实例的地址
# 例子:将请求转发到http://localhost:31200/course
#url: http://www.baidu.com #也可指定url,此url也可以是外网地址\
strip-prefix: false #true:代理转发时去掉前缀,false:代理转发时不去掉前缀
sensitiveHeaders: #默认zuul会屏蔽cookie,cookie不会传到下游服务,这里设置为空则取消默认的黑名单,如果设置了具体的头信息则不会传到下游服务
# ignoredHeaders: 默认为空表示不过虑任何头
xc-service-learning: #路由名称,名称任意,保持所有路由名称唯一
path: /learning/**
serviceId: xc-service-learning #指定服务id,从Eureka中找到服务的ip和端口
strip-prefix: false
sensitiveHeaders:
manage-cms:
path: /cms/**
serviceId: xc-service-manage-cms
strip-prefix: false
sensitiveHeaders:
manage-sys:
path: /sys/**
serviceId: xc-service-manage-cms
strip-prefix: false
sensitiveHeaders:
service-ucenter:
path: /ucenter/**
serviceId: xc-service-ucenter
sensitiveHeaders:
strip-prefix: false
xc-service-manage-order:
path: /order/**
serviceId: xc-service-manage-order
sensitiveHeaders:
strip-prefix: false
4.5 过滤器
Zuul的核心就是过滤器,通过过滤器实现请求过虑,身份校验等。
4.5.1 ZuulFilter
自定义过虑器需要继承 ZuulFilter,ZuulFilter是一个抽象类,需要覆盖它的四个方法,如下:
1、 shouldFilter:返回一个Boolean值,判断该过滤器是否需要执行。返回true表示要执行此过虑器,否则不执 行。
2、 run:过滤器的业务逻辑。
3、 filterType:返回字符串代表过滤器的类型,如下 pre:请求在被路由之前 执行 routing:在路由请求时调用 post:在routing和errror过滤器之后调用 error:处理请求时发生错误调用
4、 filterOrder:此方法返回整型数值,通过此数值来定义过滤器的执行顺序,数字越小优先级越高。
4.5.2 测试
过滤所有请求,判断头部信息是否有Authorization,如果没有则拒绝访问,否则转发到微服务。
定义过滤器,使用@Component标识为bean。
package com.xuecheng.govern.gateway.filter;
import com.alibaba.fastjson.JSON;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.ResponseResult;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author Administrator
* @version 1.0
**/
//@Component
public class LoginFilterTest extends ZuulFilter {
//过虑器的类型
@Override
public String filterType() {
/**
pre:请求在被路由之前执行
routing:在路由请求时调用
post:在routing和errror过滤器之后调用
error:处理请求时发生错误调用
*/
return "pre";
}
//过虑器序号,越小越被优先执行
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
//返回true表示要执行此过虑器
return true;
}
//过虑器的内容
//测试的需求:过虑所有请求,判断头部信息是否有Authorization,如果没有则拒绝访问,否则转发到微服务。
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
//得到request
HttpServletRequest request = requestContext.getRequest();
//得到response
HttpServletResponse response = requestContext.getResponse();
//得到Authorization头
String authorization = request.getHeader("Authorization");
if(StringUtils.isEmpty(authorization)){
//拒绝访问
requestContext.setSendZuulResponse(false);
//设置响应代码
requestContext.setResponseStatusCode(200);
//构建响应的信息
ResponseResult responseResult = new ResponseResult(CommonCode.UNAUTHENTICATED);
//转成json
String jsonString = JSON.toJSONString(responseResult);
requestContext.setResponseBody(jsonString);
//转成json,设置contentType
response.setContentType("application/json;charset=utf-8");
return null;
}
return null;
}
}
测试:
请求:http://localhost:50201/api/course/coursebase/get/4028e581617f945f01617f9dabc40000查询课程信息
Header中不设置Authorization
响应结果:
5 身份校验
5.1 需求分析
本小节实现网关连接Redis校验令牌:
1、从cookie查询用户身份令牌是否存在,不存在则拒绝访问
2、从http header查询jwt令牌是否存在,不存在则拒绝访问
3、从Redis查询user_token令牌是否过期,过期则拒绝访问
5.2 编写代码
1、配置application.yml
spring:
application:
name: xc-govern-gateway
redis:
host: ${REDIS_HOST:127.0.0.1}
port: ${REDIS_PORT:6379}
timeout: 5000 #连接超时 毫秒
jedis:
pool:
maxActive: 3
maxIdle: 3
minIdle: 1
maxWait: -1 #连接池最大等行时间 -1没有限制
2、使用StringRedisTemplate查询key的有效期
在service包下定义AuthService类:
package com.xuecheng.govern.gateway.service;
import com.xuecheng.framework.utils.CookieUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author Administrator
* @version 1.0
**/
@Service
public class AuthService {
@Autowired
StringRedisTemplate stringRedisTemplate;
//从头取出jwt令牌
public String getJwtFromHeader(HttpServletRequest request){
//取出头信息
String authorization = request.getHeader("Authorization");
if(StringUtils.isEmpty(authorization)){
return null;
}
if(!authorization.startsWith("Bearer ")){
return null;
}
//取到jwt令牌
String jwt = authorization.substring(7);
return jwt;
}
//从cookie取出token
//查询身份令牌
public String getTokenFromCookie(HttpServletRequest request){
Map<String, String> cookieMap = CookieUtil.readCookie(request, "uid");
String access_token = cookieMap.get("uid");
if(StringUtils.isEmpty(access_token)){
return null;
}
return access_token;
}
//查询令牌的有效期
public long getExpire(String access_token){
//key
String key = "user_token:"+access_token;
Long expire = stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
return expire;
}
}
说明:由于令牌存储时采用String序列化策略,所以这里用 StringRedisTemplate来查询,使用RedisTemplate无 法完成查询。
3、定义LoginFilter
package com.xuecheng.govern.gateway.filter;
import com.alibaba.fastjson.JSON;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.govern.gateway.service.AuthService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** 身份校验过虑器
* @author Administrator
* @version 1.0
**/
@Component
public class LoginFilter extends ZuulFilter {
@Autowired
AuthService authService;
//过虑器的类型
@Override
public String filterType() {
/**
pre:请求在被路由之前执行
routing:在路由请求时调用
post:在routing和errror过滤器之后调用
error:处理请求时发生错误调用
*/
return "pre";
}
//过虑器序号,越小越被优先执行
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
//返回true表示要执行此过虑器
return true;
}
//过虑器的内容
//测试的需求:过虑所有请求,判断头部信息是否有Authorization,如果没有则拒绝访问,否则转发到微服务。
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
//得到request
HttpServletRequest request = requestContext.getRequest();
//得到response
HttpServletResponse response = requestContext.getResponse();
//取cookie中的身份令牌
String tokenFromCookie = authService.getTokenFromCookie(request);
if(StringUtils.isEmpty(tokenFromCookie)){
//拒绝访问
access_denied();
return null;
}
//从header中取jwt
String jwtFromHeader = authService.getJwtFromHeader(request);
if(StringUtils.isEmpty(jwtFromHeader)){
//拒绝访问
access_denied();
return null;
}
//从redis取出jwt的过期时间
long expire = authService.getExpire(tokenFromCookie);
if(expire<0){
//拒绝访问
access_denied();
return null;
}
return null;
}
//拒绝访问
private void access_denied(){
RequestContext requestContext = RequestContext.getCurrentContext();
//得到response
HttpServletResponse response = requestContext.getResponse();
//拒绝访问
requestContext.setSendZuulResponse(false);
//设置响应代码
requestContext.setResponseStatusCode(200);
//构建响应的信息
ResponseResult responseResult = new ResponseResult(CommonCode.UNAUTHENTICATED);
//转成json
String jsonString = JSON.toJSONString(responseResult);
requestContext.setResponseBody(jsonString);
//转成json,设置contentType
response.setContentType("application/json;charset=utf-8");
}
}
5.3 测试
1、配置代理
通过nginx转发到gateway,在www.xuecheng.com虚拟主机来配置
#微服务网关
upstream api_server_pool{
server 127.0.0.1:50201 weight=10;
}
#微服务网关
location /api {
proxy_pass http://api_server_pool;
}
使用postman测试:
Get请求:http://www.xuecheng.com/api/course/coursebase/get/4028e581617f945f01617f9dabc40000
注意:这里通过网关请求了 course/coursebase/get地址,课程管理url根据自己的开发情况去配置。
1、正常流程测试
a、执行登录使之向cookie写入身份令牌uid
Post请求:http://ucenter.xuecheng.com/openapi/auth/userlogin
并从redis获取jwt令牌的内容
b、手动在postman添加header
成功查询:
2、异常流程测试
手动删除header或清除cookie观察测试结果。