基于SpringSecurity 实现的完善的登录鉴权系统
最近接触到一套完善的前后端分离的后台管理系统,这里就其登录鉴权部分做一个简单的梳理。项目地址。
模块涉及:SpringSecurity、JWT、Redis
一、整体登录鉴权流程
用户登录

接口鉴权

二、Controller的处理和登录参数的封装
AuthorizationController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| @AnonymousAccess
@PostMapping(value = "/login") public ResponseEntity<Object> login(@Validated @RequestBody AuthUserDto authUser, HttpServletRequest request) throws Exception { String password = RsaUtils.decryptByPrivateKey(RsaProperties.privateKey, authUser.getPassword()); String code = (String) redisUtils.get(authUser.getUuid()); redisUtils.del(authUser.getUuid()); if (StringUtils.isBlank(code)) { throw new BadRequestException("验证码不存在或已过期"); } if (StringUtils.isBlank(authUser.getCode()) || !authUser.getCode().equalsIgnoreCase(code)) { throw new BadRequestException("验证码错误"); } UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(authUser.getUsername(), password);
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); SecurityContextHolder.getContext().setAuthentication(authentication); String token = tokenProvider.createToken(authentication); final JwtUserDto jwtUserDto = (JwtUserDto) authentication.getPrincipal(); onlineUserService.save(jwtUserDto, token, request); Map<String,Object> authInfo = new HashMap<String,Object>(2){{ put("token", properties.getTokenStartWith() + token); put("user", jwtUserDto); }}; if(singleLogin){ onlineUserService.checkLoginOnUser(authUser.getUsername(),token); } return ResponseEntity.ok(authInfo); }
|
AuthUserDto
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Getter @Setter public class AuthUserDto {
@NotBlank private String username;
@NotBlank private String password;
private String code;
private String uuid = "";
@Override public String toString() { return "{username=" + username + ", password= ******}"; } }
|
JwtUserDto
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| @Getter @AllArgsConstructor public class JwtUserDto implements UserDetails {
private final UserDto user;
private final List<Long> dataScopes;
@JsonIgnore private final List<GrantedAuthority> authorities;
public Set<String> getRoles() { return authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet()); }
@Override @JsonIgnore public String getPassword() { return user.getPassword(); }
@Override @JsonIgnore public String getUsername() { return user.getUsername(); }
@JsonIgnore @Override public boolean isAccountNonExpired() { return true; }
@JsonIgnore @Override public boolean isAccountNonLocked() { return true; }
@JsonIgnore @Override public boolean isCredentialsNonExpired() { return true; }
@Override @JsonIgnore public boolean isEnabled() { return user.getEnabled(); } }
|
三、继承接口重写校验方法
UserDetailsServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| @RequiredArgsConstructor @Service("userDetailsService") @Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class) public class UserDetailsServiceImpl implements UserDetailsService {
private final UserService userService; private final RoleService roleService; private final DataService dataService;
@Override public JwtUserDto loadUserByUsername(String username) { UserDto user; try { user = userService.findByName(username); } catch (EntityNotFoundException e) { throw new UsernameNotFoundException("", e); } if (user == null) { throw new UsernameNotFoundException(""); } else { if (!user.getEnabled()) { throw new BadRequestException("账号未激活"); } return new JwtUserDto( user, dataService.getDeptIds(user), roleService.mapToGrantedAuthorities(user) ); } } }
|
四、在线用户的信息管理
OnlineUserService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
|
public void save(JwtUserDto jwtUserDto, String token, HttpServletRequest request){ String dept = jwtUserDto.getUser().getDept().getName(); String ip = StringUtils.getIp(request); String browser = StringUtils.getBrowser(request); String address = StringUtils.getCityInfo(ip); OnlineUserDto onlineUserDto = null; try { onlineUserDto = new OnlineUserDto(jwtUserDto.getUsername(), jwtUserDto.getUser().getNickName(), dept, browser , ip, address, EncryptUtils.desEncrypt(token), new Date()); } catch (Exception e) { e.printStackTrace(); } redisUtils.set(properties.getOnlineKey() + token, onlineUserDto, properties.getTokenValidityInSeconds()/1000); }
public void kickOut(String key){ key = properties.getOnlineKey() + key; redisUtils.del(key); }
public List<OnlineUserDto> getAll(String filter){ List<String> keys = redisUtils.scan(properties.getOnlineKey() + "*"); Collections.reverse(keys); List<OnlineUserDto> onlineUserDtos = new ArrayList<>(); for (String key : keys) { OnlineUserDto onlineUserDto = (OnlineUserDto) redisUtils.get(key); if(StringUtils.isNotBlank(filter)){ if(onlineUserDto.toString().contains(filter)){ onlineUserDtos.add(onlineUserDto); } } else { onlineUserDtos.add(onlineUserDto); } } onlineUserDtos.sort((o1, o2) -> o2.getLoginTime().compareTo(o1.getLoginTime())); return onlineUserDtos; }
public void checkLoginOnUser(String userName, String igoreToken){ List<OnlineUserDto> onlineUserDtos = getAll(userName); if(onlineUserDtos ==null || onlineUserDtos.isEmpty()){ return; } for(OnlineUserDto onlineUserDto : onlineUserDtos){ if(onlineUserDto.getUserName().equals(userName)){ try { String token =EncryptUtils.desDecrypt(onlineUserDto.getKey()); if(StringUtils.isNotBlank(igoreToken)&&!igoreToken.equals(token)){ this.kickOut(token); }else if(StringUtils.isBlank(igoreToken)){ this.kickOut(token); } } catch (Exception e) { log.error("checkUser is error",e); } } } }
|
OnlineUserDto
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| @Data @AllArgsConstructor @NoArgsConstructor public class OnlineUserDto {
private String userName;
private String nickName;
private String dept;
private String browser;
private String ip;
private String address;
private String key;
private Date loginTime; }
|
五、Token的发放
TokenProvider
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| @Slf4j @Component @RequiredArgsConstructor public class TokenProvider implements InitializingBean {
private final SecurityProperties properties; private final RedisUtils redisUtils; private static final String AUTHORITIES_KEY = "auth"; private Key key;
@Override public void afterPropertiesSet() { byte[] keyBytes = Decoders.BASE64.decode(properties.getBase64Secret()); this.key = Keys.hmacShaKeyFor(keyBytes); }
public String createToken(Authentication authentication) { String authorities = authentication.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(","));
return Jwts.builder() .setSubject(authentication.getName()) .claim(AUTHORITIES_KEY, authorities) .signWith(key, SignatureAlgorithm.HS512) .setId(IdUtil.simpleUUID()) .compact(); }
Authentication getAuthentication(String token) { Claims claims = Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); Object authoritiesStr = claims.get(AUTHORITIES_KEY); Collection<? extends GrantedAuthority> authorities = ObjectUtil.isNotEmpty(authoritiesStr) ? Arrays.stream(authoritiesStr.toString().split(",")) .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()) : Collections.emptyList();
User principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, token, authorities); }
public void checkRenewal(String token){ long time = redisUtils.getExpire(properties.getOnlineKey() + token) * 1000; Date expireDate = DateUtil.offset(new Date(), DateField.MILLISECOND, (int) time); long differ = expireDate.getTime() - System.currentTimeMillis(); if(differ <= properties.getDetect()){ long renew = time + properties.getRenew(); redisUtils.expire(properties.getOnlineKey() + token, renew, TimeUnit.MILLISECONDS); } }
public String getToken(HttpServletRequest request){ final String requestHeader = request.getHeader(properties.getHeader()); if (requestHeader != null && requestHeader.startsWith(properties.getTokenStartWith())) { return requestHeader.substring(7); } return null; } }
|
六、Token过滤器鉴权
TokenFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| @Slf4j @RequiredArgsConstructor public class TokenFilter extends GenericFilterBean {
private final TokenProvider tokenProvider;
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; String token = resolveToken(httpServletRequest); if(StrUtil.isNotBlank(token)){ OnlineUserDto onlineUserDto = null; SecurityProperties properties = SpringContextHolder.getBean(SecurityProperties.class); try { OnlineUserService onlineUserService = SpringContextHolder.getBean(OnlineUserService.class); onlineUserDto = onlineUserService.getOne(properties.getOnlineKey() + token); } catch (ExpiredJwtException e) { log.error(e.getMessage()); } if (onlineUserDto != null && StringUtils.hasText(token)) { Authentication authentication = tokenProvider.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(authentication); tokenProvider.checkRenewal(token); } } filterChain.doFilter(servletRequest, servletResponse); }
private String resolveToken(HttpServletRequest request) { SecurityProperties properties = SpringContextHolder.getBean(SecurityProperties.class); String bearerToken = request.getHeader(properties.getHeader()); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(properties.getTokenStartWith())) { return bearerToken.replace(properties.getTokenStartWith(),""); } return null; } }
|
七、配置匿名访问注解和Token过滤器链
匿名访问注解 @AnonymousAccess
1 2 3 4 5
| @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AnonymousAccess {
}
|
1 2 3 4 5 6 7 8 9 10 11 12
| @RequiredArgsConstructor public class TokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final TokenProvider tokenProvider;
@Override public void configure(HttpSecurity http) { TokenFilter customFilter = new TokenFilter(tokenProvider); http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); } }
|
八、SpringSecurity配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| @Configuration @EnableWebSecurity @RequiredArgsConstructor @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final TokenProvider tokenProvider; private final CorsFilter corsFilter; private final JwtAuthenticationEntryPoint authenticationErrorHandler; private final JwtAccessDeniedHandler jwtAccessDeniedHandler; private final ApplicationContext applicationContext;
@Bean GrantedAuthorityDefaults grantedAuthorityDefaults() { return new GrantedAuthorityDefaults(""); }
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
@Override protected void configure(HttpSecurity httpSecurity) throws Exception { Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = applicationContext.getBean(RequestMappingHandlerMapping.class).getHandlerMethods(); Set<String> anonymousUrls = new HashSet<>(); for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) { HandlerMethod handlerMethod = infoEntry.getValue(); AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class); if (null != anonymousAccess) { anonymousUrls.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); } } httpSecurity .csrf().disable() .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) .exceptionHandling() .authenticationEntryPoint(authenticationErrorHandler) .accessDeniedHandler(jwtAccessDeniedHandler)
.and() .headers() .frameOptions() .disable()
.and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and() .authorizeRequests() .antMatchers( HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/webSocket/**" ).permitAll() .antMatchers("/swagger-ui.html").permitAll() .antMatchers("/swagger-resources/**").permitAll() .antMatchers("/webjars/**").permitAll() .antMatchers("/*/api-docs").permitAll() .antMatchers("/avatar/**").permitAll() .antMatchers("/file/**").permitAll() .antMatchers("/druid/**").permitAll() .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() .antMatchers(anonymousUrls.toArray(new String[0])).permitAll() .anyRequest().authenticated() .and().apply(securityConfigurerAdapter()); }
private TokenConfigurer securityConfigurerAdapter() { return new TokenConfigurer(tokenProvider); } }
|
到此就完成了整个系统的登录模块的配置,实现了在SpringSecurity框架基础上的身份的鉴别,权限的授予,Token的发放和利用过滤器链实现Token的截取和表述,融合了Redis可以达到对所有在线用户的管理功能。达到了不再使用Session的用户无状态登录功能。