Spring Security 真正的前后分離實(shí)現(xiàn)

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)原理

  1. 用戶登錄會(huì)調(diào)用UserDetailsService對(duì)用戶名和密碼進(jìn)行檢查,返回用戶名、密碼、權(quán)限字符串列表,認(rèn)證成功后就會(huì)將用戶信息放在安全上下文中SecurityContext。
  2. 當(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返回。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容