Spring Security網(wǎng)絡(luò)上很多前后端分離的示例很多都不是完全的前后分離,而且大家實(shí)現(xiàn)的方式各不相同,有的是靠自己寫(xiě)攔截器去自己校驗(yàn)權(quán)限的,有的頁(yè)面是使用themleaf來(lái)實(shí)現(xiàn)的不是真正的前后分離,看的越多對(duì)Spring Security越來(lái)越疑惑,此篇文章要用最簡(jiǎn)單的示例實(shí)現(xiàn)出真正的前后端完全分離的權(quán)限校驗(yàn)實(shí)現(xiàn)。
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@AllArgsConstructorpublic 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("用戶名或者密碼錯(cuò)誤"); } return user; }}
4. TokenProvider
/** * JWT Token提供器 */@Slf4j@Componentpublic class TokenProvider implements InitializingBean { public static final String AUTHORITIES_KEY = "auth"; private JwtParser jwtParser; private JwtBuilder jwtBuilder; @Override public void afterPropertiesSet() { // 必須使用最少88位的Base64對(duì)該令牌進(jìn)行編碼 String secret = "必須使用最少88位的Base64對(duì)該令牌進(jìn)行編碼,一般是配置在application.yml中,需要預(yù)先定義好"; 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) { // 獲取權(quán)限列表 String authorities = authentication.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(",")); return jwtBuilder // 加入ID確保生成的 Token 都不一致 .setId(UUID.randomUUID().toString()) // 權(quán)限列表 .claim(AUTHORITIES_KEY, authorities) // username .setSubject(authentication.getName()) // 過(guò)期時(shí)間 .setExpiration(DateUtils.addDays(new Date(), 1)) .compact(); } /** * 從token中獲取認(rèn)證信息 * @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
@Componentpublic class JwtAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { // 當(dāng)用戶在沒(méi)有授權(quán)的情況下訪問(wèn)受保護(hù)的REST資源時(shí),將調(diào)用此方法發(fā)送403 Forbidden響應(yīng) response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage()); }}
6. AuthenticationEntryPoint
@Componentpublic class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { // 當(dāng)用戶嘗試訪問(wèn)安全的REST資源而不提供任何憑據(jù)時(shí),將調(diào)用此方法發(fā)送401響應(yīng) response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException == null ? "Unauthorized" : authException.getMessage()); }}
7. TokenFilter
@Slf4j@Componentpublic 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@EnableWebMvcpublic 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
@RequiredArgsConstructorpublic 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) // 授權(quán)異常 .exceptionHandling() .authenticationEntryPoint(jwtAuthenticationEntryPoint) .accessDeniedHandler(jwtAccessDeniedHandler) // 防止iframe 造成跨域 .and() .headers() .frameOptions() .disable() // 不創(chuàng)建會(huì)話 .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 靜態(tài)資源等等 .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請(qǐng)求 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 不需要認(rèn)證的接口 .antMatchers("/auth/login").permitAll() // 所有請(qǐng)求都需要認(rèn)證 .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"); // 會(huì)調(diào)用 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"; }}

訪問(wèn)有權(quán)限的接口。

訪問(wèn)沒(méi)有權(quán)限的接口被拒絕。

13. Spring Security 認(rèn)證和授權(quán)原理
- 用戶登錄會(huì)調(diào)用UserDetailsService對(duì)用戶名和密碼進(jìn)行檢查,返回用戶名、密碼、權(quán)限字符串列表,認(rèn)證成功后就會(huì)將用戶信息放在安全上下文中SecurityContext。
- 當(dāng)用戶訪問(wèn)帶有權(quán)限的接口,Spring Security會(huì)調(diào)用TokenFilter獲取到token,解析token并存入到安全上下文SecurityContext中,然后檢查@PreAuthorize("hasAnyRole('user:add')")配置的權(quán)限字符串是否在SecurityContext中用戶的authorities列表中,如果在表示有權(quán)限放行,如果不在表示沒(méi)有權(quán)限,則執(zhí)行AccessDeniedHandler返回。