- 用户登录的认证和授权,流程如下:
- 为什么使用Redis储存登录凭证?
后台在每次处理请求的时候都要查询用户的登录凭证,访问的频率非常高 - 为什么使用Redis存储用户信息?
将user缓存到Redis中,获取user时,先从Redis获取。取不到时,则从数据库中查询,再缓存到Redis中。因为很多界面都要用到user信息,并发时,频繁的访问数据库,会导致数据库崩溃。若变更数据库,需要先更新数据库,再清空缓存。
- 登录认证的主要实现
//登录认证的service层
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RoleService roleService;
@Autowired
private ResourceService resourceService;
@Autowired
private JwtTokenManagerProperties jwtTokenManagerProperties;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public UserVo login(LoginDto loginDto) {
//认证管理器--认证用户(需准备一个类实现UserDetailsService接口,来从数据库中查询用户信息,重写loadUserByUsername方法)
UsernamePasswordAuthenticationToken upat =
new UsernamePasswordAuthenticationToken(loginDto.getUsername(),loginDto.getPassword());
Authentication authenticate = authenticationManager.authenticate(upat);
//是否验证成功
if(!authenticate.isAuthenticated()){
throw new BaseException("登录失败");
}
//获取用户信息
UserAuth userAuth = (UserAuth) authenticate.getPrincipal();
//对象拷贝
UserVo userVo = BeanUtil.toBean(userAuth, UserVo.class);
//获取资源列表(请求的路径,只有类型为r才是真正的请求按钮,也就是访问路径)
List<ResourceVo> resourceVoList = resourceService.findResourceVoListByUserId(userVo.getId());
Set<String> resourcePathsSet = resourceVoList.stream()
.filter(x->"r".equals(x.getResourceType())) //资源类型r
.map(ResourceVo::getRequestPath)
.collect(Collectors.toSet());
userVo.setResourceRequestPaths(resourcePathsSet);
//获取角色列表
List<RoleVo> roleVoList = roleService.findRoleVoListByUserId(userVo.getId());
Set<String> roleLabelSet = roleVoList.stream().map(RoleVo::getLabel).collect(Collectors.toSet());
userVo.setRoleLabels(roleLabelSet);
//密码设置为空
userVo.setPassword("");
Map<String,Object> clamis = new HashMap<>();
String userVoString = JSONUtil.toJsonStr(userVo);
clamis.put("currentUser",userVoString);
//生成token
String jwtToken = JwtUtil.createJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), jwtTokenManagerProperties.getTtl(), clamis);
//生成uuid
String uuidToken = UUID.randomUUID().toString();
userVo.setUserToken(uuidToken);
//拼接key值 加前缀
String userTokenKey = UserCacheConstant.USER_TOKEN + userVo.getUsername();
String jwtTokenKey = UserCacheConstant.JWT_TOKEN + uuidToken;
//设置过期时间
long ttl = Long.valueOf(jwtTokenManagerProperties.getTtl() / 1000);
//存储redis username:uuid
redisTemplate.opsForValue().set(userTokenKey, uuidToken,ttl, TimeUnit.SECONDS);
//存储redis uuid:jwttoken 方便后登录的用户替换旧登录的用户
redisTemplate.opsForValue().set(jwtTokenKey,jwtToken,ttl,TimeUnit.SECONDS);
//返回vo
return userVo;
}
}
然后,配置请求地址
@Configuration
@EnableConfigurationProperties(SecurityConfigProperties.class)
public class SecurityConfig {
@Autowired
JwtAuthorizationManager jwtAuthorizationManager;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//忽略地址
http.authorizeHttpRequests()
.antMatchers( "/security/login" )
.permitAll();
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS );//关闭session
http.headers().cacheControl().disable();//关闭缓存
return http.build();
}
//任务管理器
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
//BCrypt密码编码
@Bean
public BCryptPasswordEncoder bcryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
- 用户数据存入线程中,当用户请求其他业务需要当前用户信息的时候,可以直接获取当前登录人信息。新增拦截器,该流程如下:
//拦截器
@Component
public class UserTokenIntercept implements HandlerInterceptor {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private JwtTokenManagerProperties jwtTokenManagerProperties;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//是否是handle,不是即放行
if (!(handler instanceof HandlerMethod)) {
return true;
}
//从头部中拿到当前userToken
String userToken = request.getHeader(SecurityConstant.USER_TOKEN);
if (!EmptyUtil.isNullOrEmpty(userToken)) {
String jwtTokenKey = UserCacheConstant.JWT_TOKEN + userToken;
//redis中获取
String jwtToken = redisTemplate.opsForValue().get(jwtTokenKey);
if (!EmptyUtil.isNullOrEmpty(jwtToken)) {
//解析jwt
Object userObj = JwtUtil.parseJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), jwtToken).get("currentUser");
String currentUser = String.valueOf(userObj);
//放入当前线程中:用户当前的web直接获得user使用
UserThreadLocal.setSubject(currentUser);
}
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除当前线程中的参数
UserThreadLocal.removeSubject();
}
}
然后,配置web的config文件,使这个拦截器生效。
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
UserTokenIntercept userTokenIntercept;
//拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
//userToken拦截
registry.addInterceptor(userTokenIntercept).excludePathPatterns("无需拦截的路径").addPathPatterns("/**");
- 验证登录-自定义授权管理器
@Component
public class JwtAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private JwtTokenManagerProperties jwtTokenManagerProperties;
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext requestAuthorizationContext) {
//用户当前请求路径 GET/nursing_project/**
String method = requestAuthorizationContext.getRequest().getMethod();
String requestURI = requestAuthorizationContext.getRequest().getRequestURI();
String targetUrl = (method+requestURI);
//获得请求中的认证后传递过来的userToken
String userToken = requestAuthorizationContext.getRequest().getHeader(SecurityConstant.USER_TOKEN);
//如果userToken为空,则当前请求不合法
if (EmptyUtil.isNullOrEmpty(userToken)){
return new AuthorizationDecision(false);
}
//通过userToken获取jwtToken
String jwtTokenKey = UserCacheConstant.JWT_TOKEN+userToken;
//key:uuid
String jwtToken = redisTemplate.opsForValue().get(jwtTokenKey);
//如果jwtToken为空,则当前请求不合法
if (EmptyUtil.isNullOrEmpty(jwtToken)){
return new AuthorizationDecision(false);
}
//校验jwtToken是否合法
Claims cla = JwtUtil.parseJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), jwtToken);
if (ObjectUtil.isEmpty(cla)) {
//token失效
return new AuthorizationDecision(false);
}
//如果校验jwtToken通过,则获得userVo对象
UserVo userVo = JSONObject.parseObject(String.valueOf(cla.get("currentUser")),UserVo.class);
//用户剔除校验:redis中最新的userToken与前端传入的userToken不符合,则认为当前用户被后续用户剔除
//key:username value:uuid
String currentUserToken = redisTemplate.opsForValue().get(UserCacheConstant.USER_TOKEN + userVo.getUsername());
if (!userToken.equals(currentUserToken)){
return new AuthorizationDecision(false);
}
//如果当前UserToken存活时间少于10分钟,则进行续期
Long remainTimeToLive = redisTemplate.opsForValue().getOperations().getExpire(jwtTokenKey);
if (remainTimeToLive.longValue()<= 600){
//续期:jwtToken需要重新生成;userToken只需要重新设置过期时间
//jwt生成的token也会过期,所以需要重新生成jwttoken
Map<String, Object> claims = new HashMap<>();
String userVoJsonString = String.valueOf(cla.get("currentUser"));
claims.put("currentUser", userVoJsonString);
//jwtToken令牌颁布
String newJwtToken = JwtUtil.createJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), jwtTokenManagerProperties.getTtl(), claims);
long ttl = Long.valueOf(jwtTokenManagerProperties.getTtl()) / 1000;
//重新存入
redisTemplate.opsForValue().set(jwtTokenKey, newJwtToken, ttl, TimeUnit.SECONDS);
//设置过期时间
redisTemplate.expire(UserCacheConstant.USER_TOKEN + userVo.getUsername(), ttl, TimeUnit.SECONDS);
}
//当前用户资源是否包含当前URL
for (String resourceRequestPath : userVo.getResourceRequestPaths()) {
boolean isMatch = antPathMatcher.match(resourceRequestPath, targetUrl);
if (isMatch){
return new AuthorizationDecision(true);
}
}
return new AuthorizationDecision(false);
}
}
然后,修改配置文件中部分代码,来过滤请求,加载授权管理器
//忽略地址
List<String> ignoreUrl = securityConfigProperties.getIgnoreUrl();//从配置文件中获得地址
http.authorizeHttpRequests()
.antMatchers( ignoreUrl.toArray( new String[ignoreUrl.size() ] ) )
.permitAll()
.anyRequest().access(jwtAuthorizationManager);