zoukankan      html  css  js  c++  java
  • Keep三面:如何用Spring Security实现前后端分离?

    前言

    Spring Security网络上很多前后端分离的示例很多都不是完全的前后分离,而且大家实现的方式各不相同,有的是靠自己写拦截器去自己校验权限的,有的页面是使用themleaf来实现的不是真正的前后分离,看的越多对Spring Security越来越疑惑,此篇文章要用最简单的示例实现出真正的前后端完全分离的权限校验实现,spring全家桶共享,总结了大厂面试真题,资深架构师学习笔记等。

    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 认证和授权原理

    1. 用户登录会调用UserDetailsService对用户名和密码进行检查,返回用户名、密码、权限字符串列表,认证成功后就会将用户信息放在安全上下文中SecurityContext。
    2. 当用户访问带有权限的接口,Spring Security会调用TokenFilter获取到token,解析token并存入到安全上下文SecurityContext中,然后检查@PreAuthorize("hasAnyRole('user:add')")配置的权限字符串是否在SecurityContext中用户的authorities列表中,如果在表示有权限放行,如果不在表示没有权限,则执行AccessDeniedHandler返回。
    3. 关注公众号:麒麟改bug,共享更多Java相关的学习笔记,面试真题,电子书,感谢您的支持!
  • 相关阅读:
    外包、构件和黑盒抽象等杂想
    C++类型转换小记(一)——C++转换操作符
    大学(一)
    【答】如何获取一个【备份路径】的信息?
    橘色超漂亮滑动二级导航菜单
    CSS自适应宽度按钮
    我们忽略的IE特效——一些特殊效果
    MSSQL 游标示例
    [存]超酷JS拖拽翻页效果
    漂亮的表格
  • 原文地址:https://www.cnblogs.com/QLCZ/p/14903045.html
Copyright © 2011-2022 走看看