权限控制是非常常见的功能,在各种后台管理里权限控制更是重中之重。在 Spring Boot 中使用 Spring Security 构建权限系统是非常轻松和简单的。Spring Security 是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在 Spring 应用上下文中配置的 Bean,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
添加依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > <version > 0.9.0</version > </dependency > <dependency > <groupId > org.springframework.security</groupId > <artifactId > spring-security-test</artifactId > <scope > test</scope > </dependency >
UserDetails 按照官方文档的说法,为了定义我们自己的认证管理,我们可以添加 UserDetailsService
, AuthenticationProvider
, AuthenticationManager
这种类型的 Bean。实现的方式有多种,这里我选择最简单的一种(因为本身我们这里的认证授权也比较简单)通过定义自己的 UserDetailsService
从数据库查询用户信息,至于认证的话就用默认的。
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 public class User implements UserDetails { private Integer id; private String username; private String password; private Date lastPasswordResetDate; private List<Role> roles = new ArrayList <>(); public Integer getId () { return id; } public void setId (Integer id) { this .id = id; } public void setUsername (String username) { this .username = username; } public void setPassword (String password) { this .password = password; } @JsonIgnore public List<Role> getRoles () { return roles; } public void setRoles (List<Role> roles) { this .roles = roles; } @Override @JsonIgnore public Collection<? extends GrantedAuthority > getAuthorities() { return roles.stream().map(Role::getName).map(SimpleGrantedAuthority::new ).collect(Collectors.toList()); } @Override public String getPassword () { return password; } @Override public String getUsername () { return username; } @Override @JsonIgnore public boolean isAccountNonExpired () { return true ; } @Override @JsonIgnore public boolean isAccountNonLocked () { return true ; } @Override @JsonIgnore public boolean isCredentialsNonExpired () { return true ; } @Override @JsonIgnore public boolean isEnabled () { return true ; } @JsonIgnore public Date getLastPasswordResetDate () { return lastPasswordResetDate; } public void setLastPasswordResetDate (Date lastPasswordResetDate) { this .lastPasswordResetDate = lastPasswordResetDate; } }
开启权限验证 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 @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Resource private UserDetailsService userDetailsService; @Resource private AuthenticationTokenFilter authenticationTokenFilter; @Override public void configure (AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder.userDetailsService(this .userDetailsService).passwordEncoder(passwordEncoder()); } @Override protected void configure (HttpSecurity httpSecurity) throws Exception { httpSecurity.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() .antMatchers("/authentication/**" ).permitAll() .anyRequest().authenticated(); httpSecurity.headers().cacheControl(); httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } @Bean @Override public AuthenticationManager authenticationManagerBean () throws Exception { return super .authenticationManagerBean(); } @Bean public PasswordEncoder passwordEncoder () { return new BCryptPasswordEncoder (); } }
集成 JWT 想要将 JWT(Json Web Token) 在 spring boot 中集成起来,需要创建一个 filter,继承自 OncePerRequestFilter。 并且将它添加到 WebSecurityConfig,需要注意的是,这个过滤器是作为权限验证,必须添加到 before 集合中。
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 @Component public class AuthenticationTokenFilter extends OncePerRequestFilter { @Resource private UserDetailsService userDetailsService; @Resource private TokenUtil tokenUtil; @Override protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authorization = request.getHeader(AUTHORIZATION); if (!StringUtils.isEmpty(authorization) && authorization.startsWith(TokenUtil.TOKEN_PREFIX)) { String token = authorization.substring(TokenUtil.TOKEN_PREFIX.length()); String username = tokenUtil.getUsernameFromToken(token); if (!StringUtils.isEmpty(username) && SecurityContextHolder.getContext().getAuthentication() == null ) { UserDetails userDetails = this .userDetailsService.loadUserByUsername(username); if (tokenUtil.validateToken(token, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken ( userDetails, null , userDetails.getAuthorities() ); authentication.setDetails(new WebAuthenticationDetailsSource ().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } } filterChain.doFilter(request, response); } }
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 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 @Component public class TokenUtil implements Serializable { public static final String TOKEN_PREFIX = "Bearer " ; static final String CLAIM_KEY_USERNAME = "sub" ; static final String CLAIM_KEY_CREATED = "iat" ; private static final long serialVersionUID = -3301605591108950415L ; private Clock clock = DefaultClock.INSTANCE; @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; public String getUsernameFromToken (String token) { return getClaimFromToken(token, Claims::getSubject); } public Date getIssuedAtDateFromToken (String token) { return getClaimFromToken(token, Claims::getIssuedAt); } public Date getExpirationDateFromToken (String token) { return getClaimFromToken(token, Claims::getExpiration); } public <T> T getClaimFromToken (String token, Function<Claims, T> claimsResolver) { final Claims claims = getAllClaimsFromToken(token); return claimsResolver.apply(claims); } private Claims getAllClaimsFromToken (String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } private Boolean isTokenExpired (String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(clock.now()); } private Boolean isCreatedBeforeLastPasswordReset (Date created, Date lastPasswordReset) { return (lastPasswordReset != null && created.before(lastPasswordReset)); } private Boolean ignoreTokenExpiration (String token) { return false ; } public String generateToken (UserDetails userDetails) { Map<String, Object> claims = new HashMap <>(); return doGenerateToken(claims, userDetails.getUsername()); } private String doGenerateToken (Map<String, Object> claims, String subject) { final Date createdDate = clock.now(); final Date expirationDate = calculateExpirationDate(createdDate); return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(createdDate) .setExpiration(expirationDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public Boolean canTokenBeRefreshed (String token, Date lastPasswordReset) { final Date created = getIssuedAtDateFromToken(token); return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset) && (!isTokenExpired(token) || ignoreTokenExpiration(token)); } public String refreshToken (String token) { final Date createdDate = clock.now(); final Date expirationDate = calculateExpirationDate(createdDate); final Claims claims = getAllClaimsFromToken(token); claims.setIssuedAt(createdDate); claims.setExpiration(expirationDate); return Jwts.builder() .setClaims(claims) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public Boolean validateToken (String token, UserDetails userDetails) { if (userDetails instanceof User) { User user = (User) userDetails; final String username = getUsernameFromToken(token); final Date created = getIssuedAtDateFromToken(token); return ( username.equals(user.getUsername()) && !isTokenExpired(token) && !isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate()) ); } else { System.out.println("类型转换失败!" ); return false ; } } private Date calculateExpirationDate (Date createdDate) { return new Date (createdDate.getTime() + expiration * 1000 ); } }
UserDetailsService 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 @Service public class UserDetailsServiceImpl implements UserDetailsService { @Resource private UserMapper userMapper; @Resource private RoleMapper roleMapper; @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { User user = userMapper.findByUsername(username); if (user == null ) throw new UsernameNotFoundException ("Cannot find user with username, username = " + username); user.setRoles(roleMapper.selectByUserId(user.getId())); return user; } }