Spring Security 真正的前后分离实现
Spring Security网络上很多前后端分离的示例很多都不是完全的前后分离,而且大家实现的方式各不相同,有的是靠自己写拦截器去自己校验权限的,有的页面是使用themleaf来实现的不是真正的前后分离,看的越多对Spring Security越来越疑惑,此篇文章要用最简单的示例实现出真正的前后端完全分离的权限校验实现。
1. pom.xml
主要依赖是spring-boot-starter-security和jwt。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>${jjwt.version}</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>${jjwt.version}</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>${jjwt.version}</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.9</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
2. User
@Data @ToString @NoArgsConstructor @AllArgsConstructor public class User implements UserDetails { private Long id; private String username; private String password; private Boolean enabled; private List<GrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return this.username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return this.enabled; } }
3. UserDetailsService
@RequiredArgsConstructor @Service("userDetailsService") public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public User loadUserByUsername(String username) { List<GrantedAuthority> authorities = Arrays.asList( new SimpleGrantedAuthority("user:add"), new SimpleGrantedAuthority("user:view"), new SimpleGrantedAuthority("user:update")); User user = new User(1L, username, passwordEncoder.encode("123456"), true, authorities); if (user == null) { throw new UsernameNotFoundException("用户名或者密码错误"); } return user; } }
4. TokenProvider
/** * JWT Token提供器 */ @Slf4j @Component public class TokenProvider implements InitializingBean { public static final String AUTHORITIES_KEY = "auth"; private JwtParser jwtParser; private JwtBuilder jwtBuilder; @Override public void afterPropertiesSet() { // 必须使用最少88位的Base64对该令牌进行编码 String secret = "必须使用最少88位的Base64对该令牌进行编码,一般是配置在application.yml中,需要预先定义好"; byte[] keyBytes = Decoders.BASE64.decode(secret); Key key = Keys.hmacShaKeyFor(keyBytes); jwtParser = Jwts.parserBuilder().setSigningKey(key).build(); jwtBuilder = Jwts.builder().signWith(key, SignatureAlgorithm.HS512); } public String createToken(Authentication authentication) { // 获取权限列表 String authorities = authentication.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(",")); return jwtBuilder // 加入ID确保生成的 Token 都不一致 .setId(UUID.randomUUID().toString()) // 权限列表 .claim(AUTHORITIES_KEY, authorities) // username .setSubject(authentication.getName()) // 过期时间 .setExpiration(DateUtils.addDays(new Date(), 1)) .compact(); } /** * 从token中获取认证信息 * @param token * @return */ public Authentication getAuthentication(String token) { Claims claims = jwtParser.parseClaimsJws(token).getBody(); Object authoritiesStr = claims.get(AUTHORITIES_KEY); Collection<? extends GrantedAuthority> authorities = authoritiesStr != null ? 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); } }
5. AccessDeniedHandler
@Component public class JwtAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { // 当用户在没有授权的情况下访问受保护的REST资源时,将调用此方法发送403 Forbidden响应 response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage()); } }
6. AuthenticationEntryPoint
@Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { // 当用户尝试访问安全的REST资源而不提供任何凭据时,将调用此方法发送401响应 response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException == null ? "Unauthorized" : authException.getMessage()); } }
7. TokenFilter
@Slf4j @Component public class TokenFilter extends GenericFilterBean { private TokenProvider tokenProvider; public TokenFilter(TokenProvider tokenProvider) { this.tokenProvider = tokenProvider; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; String bearerToken = httpServletRequest.getHeader("Authorization"); String token = null; if (!StringUtils.isEmpty(bearerToken) && bearerToken.startsWith("Bearer")) { token = bearerToken.replace("Bearer", ""); } if (!StringUtils.isEmpty(token)) { Authentication authentication = tokenProvider.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(servletRequest, servletResponse); } }
8. WebMvcConfigurer
@Configuration @EnableWebMvc public class WebMvcConfigurerAdapter implements WebMvcConfigurer { @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }
9. TokenConfigurer
@RequiredArgsConstructor public class TokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { private TokenProvider tokenProvider; public TokenConfigurer(TokenProvider tokenProvider) { this.tokenProvider = tokenProvider; } @Override public void configure(HttpSecurity http) { TokenFilter customFilter = new TokenFilter(tokenProvider); http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); } }
10. SecurityConfig
@Configuration @EnableWebSecurity @RequiredArgsConstructor @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CorsFilter corsFilter; @Autowired private TokenProvider tokenProvider; @Autowired private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowired private JwtAccessDeniedHandler jwtAccessDeniedHandler; @Bean public GrantedAuthorityDefaults grantedAuthorityDefaults() { // 去除 ROLE_ 前缀 return new GrantedAuthorityDefaults(""); } @Bean public PasswordEncoder passwordEncoder() { // 密码加密方式 return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity // 禁用 CSRF .csrf().disable() .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) // 授权异常 .exceptionHandling() .authenticationEntryPoint(jwtAuthenticationEntryPoint) .accessDeniedHandler(jwtAccessDeniedHandler) // 防止iframe 造成跨域 .and() .headers() .frameOptions() .disable() // 不创建会话 .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 静态资源等等 .antMatchers( HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/webSocket/**" ).permitAll() // swagger 文档 .antMatchers("/swagger-ui.html").permitAll() .antMatchers("/swagger-resources/**").permitAll() .antMatchers("/webjars/**").permitAll() .antMatchers("/*/api-docs").permitAll() // 文件 .antMatchers("/avatar/**").permitAll() .antMatchers("/file/**").permitAll() // 阿里巴巴 druid .antMatchers("/druid/**").permitAll() // 放行OPTIONS请求 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 不需要认证的接口 .antMatchers("/auth/login").permitAll() // 所有请求都需要认证 .anyRequest().authenticated() .and().apply(securityConfigurerAdapter()); } private TokenConfigurer securityConfigurerAdapter() { return new TokenConfigurer(tokenProvider); } }
11. AuthController
@RestController @RequestMapping("/auth") public class AuthController { @Autowired private TokenProvider tokenProvider; @Autowired private AuthenticationManagerBuilder authenticationManagerBuilder; @RequestMapping("/login") public String login() { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken("monday", "123456"); // 会调用 UserDetailsService.loadUserByUsername Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); SecurityContextHolder.getContext().setAuthentication(authentication); String token = tokenProvider.createToken(authentication); return token; } }
12. UserController
@RestController @RequestMapping("/user") public class UserController { @RequestMapping("/add") @PreAuthorize("hasAnyRole('user:add')") public String add() { return "user:add"; } @RequestMapping("/update") @PreAuthorize("hasAnyRole('user:update')") public String update() { return "user:update"; } @RequestMapping("/view") @PreAuthorize("hasAnyRole('user:view')") public String view() { return "user:view"; } @RequestMapping("/delete") @PreAuthorize("hasAnyRole('user:delete')") public String delete() { return "user:delete"; } }
访问有权限的接口。
访问没有权限的接口被拒绝。
13. Spring Security 认证和授权原理
- 用户登录会调用UserDetailsService对用户名和密码进行检查,返回用户名、密码、权限字符串列表,认证成功后就会将用户信息放在安全上下文中SecurityContext。
- 当用户访问带有权限的接口,Spring Security会调用TokenFilter获取到token,解析token并存入到安全上下文SecurityContext中,然后检查@PreAuthorize("hasAnyRole('user:add')")配置的权限字符串是否在SecurityContext中用户的authorities列表中,如果在表示有权限放行,如果不在表示没有权限,则执行AccessDeniedHandler返回。