用户认证
用户认证流程分析
用户认证流程如下:
业务流程说明如下:
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令牌,完成授权,拥有权限的方法正常执行,没有权限的方法将拒绝访问。
查询用户接口
Api接口
用户中心对外提供如下接口:
1、响应数据类型
此接口将来被用来查询用户信息及用户权限信息,所以这里定义扩展类型
@Data
@ToString
public class XcUserExt extends XcUser {
//权限信息
private List<XcMenu> permissions;
//企业信息
private String companyId;
}
2、根据账号查询用户信息
@Api(value = "用户中心",description = "用户中心管理")
public interface UcenterControllerApi {
public XcUserExt getUserext(String username);
}
DAO
添加XcUser、XcCompantUser两个表的Dao
public interface XcUserRepository extends JpaRepository<XcUser, String> {
XcUser findXcUserByUsername(String username);
}
public interface XcCompanyUserRepository extends JpaRepository<XcCompanyUser,String> {
//根据用户id查询所属企业id
XcCompanyUser findByUserId(String userId);
}
Service
@Service
public class UserService {
@Autowired
private XcUserRepository xcUserRepository;
//根据用户账号查询用户信息
public XcUser findXcUserByUsername(String username){
return xcUserRepository.findXcUserByUsername(username);
}
//根据账号查询用户的信息,返回用户扩展信息
public XcUserExt getUserExt(String username){
XcUser xcUser = this.findXcUserByUsername(username);
if(xcUser == null){
return null;
}
XcUserExt xcUserExt = new XcUserExt();
BeanUtils.copyProperties(xcUser,xcUserExt);
//用户id
String userId = xcUserExt.getId();
//查询用户所属公司
XcCompanyUser xcCompanyUser = xcCompanyUserRepository.findXcCompanyUserByUserId(userId);
if(xcCompanyUser!=null){
String companyId = xcCompanyUser.getCompanyId();
xcUserExt.setCompanyId(companyId);
}
return xcUserExt;
}
}
Controller
@RestController
@RequestMapping("/ucenter")
public class UcenterController implements UcenterControllerApi {
@Autowired
UserService userService;
@Override
@GetMapping("/getuserext")
public XcUserExt getUserext(@RequestParam("username") String username) {
XcUserExt xcUser = userService.getUserExt(username);
return xcUser;
}
}
调用查询用户接口
创建client
认证服务需要远程调用用户中心服务查询用户,在认证服务中创建Feign客户端
@FeignClient(value = XcServiceList.XC_SERVICE_UCENTER)
public interface UserClient {
@GetMapping("/ucenter/getuserext")
public XcUserExt getUserext(@RequestParam("username") String username)
}
UserDetailsServiceImpl
认证服务调用spring security接口申请令牌,spring security接口会调用UserDetailsServiceImpl从数据库查询用
户,如果查询不到则返回 NULL,表示不存在;在UserDetailsServiceImpl中将正确的密码返回, spring security
会自动去比对输入密码的正确性。
1、修改UserDetailsServiceImpl的loadUserByUsername方法,调用Ucenter服务的查询用户接口
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserClient userClient;
@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;
}
//请求ucenter查询用户
XcUserExt userext = userClient.getUserext(username);
if(userext == null){
//返回NULL表示用户不存在,Spring Security会抛出异常
return null;
}
//从数据库查询用户正确的密码,Spring Security会去比对输入密码的正确性
String password = userext.getPassword();
String user_permission_string = "";
UserJwt userDetails = new UserJwt(username,
password,
AuthorityUtils.commaSeparatedStringToAuthorityList(user_permission_string));
//用户id
userDetails.setId(userext.getId());
//用户名称
userDetails.setName(userext.getName());
//用户头像
userDetails.setUserpic(userext.getUserpic());
//用户所属企业id
userDetails.setCompanyId(userext.getCompanyId());
return userDetails;
}
}
BCryptPasswordEncoder
早期使用md5对密码进行编码,每次算出的md5值都一样,这样非常不安全,Spring Security推荐使用
BCryptPasswordEncoder对密码加随机盐,每次的Hash值都不一样,安全性高。
1、BCryptPasswordEncoder测试程序如下
@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
//采用bcrypt对密码进行Hash
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
前端显示当前用户
需求分析
用户登录成功在页头显示当前登录的用户名称。
数据流程如下图:
1、用户请求认证服务,登录成功。
2、用户登录成功,认证服务向cookie写入身份令牌,向redis写入user_token(身份令牌及授权jwt授权令牌)
3、客户端携带cookie中的身份令牌请求认证服务获取jwt令牌。
4、客户端解析jwt令牌,并将解析的用户信息存储到sessionStorage中。
jwt令牌中包括了用户的基本信息,客户端解析jwt令牌即可获取用户信息。
5、客户端从sessionStorage中读取用户信息,并在页头显示。
jwt 查询接口
需求分析
认证服务对外提供jwt查询接口,流程如下:
1 、客户端携带cookie中的身份令牌请求认证服务获取jwt
2、认证服务根据身份令牌从redis中查询jwt令牌并返回给客户端。
API
在认证模块定义 jwt查询接口:
@Api(value = "jwt查询接口",description = "客户端查询jwt令牌内容")
public interface AuthControllerApi {
@ApiOperation("查询userjwt令牌")
public JwtResult userjwt();
Service
在AuthService中定义方法如下:
//从redis查询令牌
public AuthToken getUserToken(String token){
String userToken = "user_token:"+token;
String userTokenString = stringRedisTemplate.opsForValue().get(userToken);
if(userToken!=null){
AuthToken authToken = null;
try {
authToken = JSON.parseObject(userTokenString, AuthToken.class);
} catch (Exception e) {
LOGGER.error("getUserToken from redis and execute JSON.parseObject error
{}",e.getMessage());
e.printStackTrace();
}
return authToken;
}
return null;
}
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(){
Map<String, String> cookieMap = CookieUtil.readCookie(request, "uid");
String access_token = cookieMap.get("uid");
return access_token;
}
测试
使用postman测试
1、请求 /auth/userlogin
观察cookie是否已存入用户身份令牌。
2、get请求jwt
用户退出
需求分析
操作流程如下:
1、用户点击退出,弹出退出确认窗口,点击确定
2、退出成功
用户退出要以下动作:
1、删除redis中的token
2、删除cookie中的token
API
认证服务对外提供退出接口。
@ApiOperation("退出")
public ResponseResult logout();
服务端
认证服务提供退出接口。
Service
//从redis中删除令牌
public boolean delToken(String access_token){
String name = "user_token:" + access_token;
stringRedisTemplate.delete(name);
return true;
}
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){
CookieUtil.addCookie(response, cookieDomain, "/", "uid", token, 0, false);
}
退出URL放行
认证服务默认都要校验用户的身份信息,这里需要将退出url放行。
在WebSecurityConfig类中重写 configure(WebSecurity web)方法,如下:
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/userlogin","/userlogout");
}
Zuul 网关
Zuul 介绍
什么是Zuul?
Spring Cloud Zuul是整合Netflix公司的Zuul开源项目实现的微服务网关,它实现了请求路由、负载均衡、校验过
虑等 功能。
什么是网关?
服务网关是在微服务前边设置一道屏障,请求先到服务网关,网关会对请求进行过虑、校验、路由等处理。有了服
务网关可以提高微服务的安全性,网关校验请求的合法性,请求不合法将被拦截,拒绝访问。
Zuul与Nginx怎么配合使用?
Zuul与Nginx在实际项目中需要配合使用,如下图,Nginx的作用是反向代理、负载均衡,Zuul的作用是保障微服
务的安全访问,拦截微服务请求,校验合法性及负载均衡。
路由配置
需求分析
Zuul网关具有代理的功能,根据请求的url转发到微服务,如下图:
客户端请求网关/api/learning,通过路由转发到/learning
客户端请求网关/api/course,通过路由转发到/course
路由配置
在appcation.yml中配置:
zuul:
routes:
manage‐course: #路由名称,名称任意,保持所有路由名称唯一
path: /course/**
serviceId: xc‐service‐manage‐course #指定服务id,从Eureka中找到服务的ip和端口
#url: http://localhost:31200 #也可指定url
strip‐prefix: false #true:代理转发时去掉前缀,false:代理转发时不去掉前缀
sensitiveHeaders: #默认zuul会屏蔽cookie,cookie不会传到下游服务,这里设置为空则取消默认的黑名
单,如果设置了具体的头信息则不会传到下游服务
# ignoredHeaders: Authorization
serviceId:推荐使用serviceId,zuul会从Eureka中找到服务id对应的ip和端口。
strip-prefix: false #true:代理转发时去掉前缀,false:代理转发时不去掉前缀,例如,为true请
求/course/coursebase/get/..,代理转发到/coursebase/get/,如果为false则代理转发到/course/coursebase/get
sensitiveHeaders :敏感头设置,默认会过虑掉cookie,这里设置为空表示不过虑
ignoredHeaders:可以设置过虑的头信息,默认为空表示不过虑任何头
完整的路由配置
zuul:
routes:
xc‐service‐learning: #路由名称,名称任意,保持所有路由名称唯一
path: /learning/**
serviceId: xc‐service‐learning #指定服务id,从Eureka中找到服务的ip和端口
strip‐prefix: false
sensitiveHeaders:
manage‐course:
path: /course/**
serviceId: xc‐service‐manage‐course
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
过滤器
Zuul的核心就是过虑器,通过过虑器实现请求过虑,身份校验等。
ZuulFilter
自定义过虑器需要继承 ZuulFilter,ZuulFilter是一个抽象类,需要覆盖它的四个方法,如下:
1、 shouldFilter:返回一个Boolean值,判断该过滤器是否需要执行。返回true表示要执行此过虑器,否则不执
行。 2、 run:过滤器的业务逻辑。 3、 filterType:返回字符串代表过滤器的类型,如下 pre:请求在被路由之前
执行 routing:在路由请求时调用 post:在routing和errror过滤器之后调用 error:处理请求时发生错误调用
4、 filterOrder:此方法返回整型数值,通过此数值来定义过滤器的执行顺序,数字越小优先级越高。
测试
过虑所有请求,判断头部信息是否有Authorization,如果没有则拒绝访问,否则转发到微服务。
定义过虑器,使用@Component标识为bean。
@Component
public class LoginFilterTest extends ZuulFilter {
private static final Logger LOG = LoggerFactory.getLogger(LoginFilterTest.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 2;//int值来定义过滤器的执行顺序,数值越小优先级越高
}
@Override
public boolean shouldFilter() {// 该过滤器需要执行
return true;
}
@Override
public Object run() {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletResponse response = requestContext.getResponse();
HttpServletRequest request = requestContext.getRequest();
//取出头部信息Authorization
String authorization = request.getHeader("Authorization");
if(StringUtils.isEmpty(authorization)){
requestContext.setSendZuulResponse(false);// 拒绝访问
requestContext.setResponseStatusCode(200);// 设置响应状态码
ResponseResult unauthenticated = new ResponseResult(CommonCode.UNAUTHENTICATED);
String jsonString = JSON.toJSONString(unauthenticated);
requestContext.setResponseBody(jsonString);
requestContext.getResponse().setContentType("application/json;charset=UTF‐8");
return null;
}
return null;
}
}
测试:
请求:http://localhost:50201/api/course/coursebase/get/4028e581617f945f01617f9dabc40000查询课程信息