zoukankan      html  css  js  c++  java
  • Springboot集成Spring Security实现JWT认证

    我最新最全的文章都在 南瓜慢说 www.pkslow.com ,欢迎大家来喝茶!

    1 简介

    Spring Security作为成熟且强大的安全框架,得到许多大厂的青睐。而作为前后端分离的SSO方案,JWT也在许多项目中应用。本文将介绍如何通过Spring Security实现JWT认证。

    用户与服务器交互大概如下:

    1. 客户端获取JWT,一般通过POST方法把用户名/密码传给server
    2. 服务端接收到客户端的请求后,会检验用户名/密码是否正确,如果正确则生成JWT并返回;不正确则返回错误;
    3. 客户端拿到JWT后,在有效期内都可以通过JWT来访问资源了,一般把JWT放在请求头;一次获取,多次使用;
    4. 服务端校验JWT是否合法,合法则允许客户端正常访问,不合法则返回401。

    2 项目整合

    我们把要整合的Spring SecurityJWT加入到项目的依赖中去:

    <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</artifactId>
      <version>0.9.1</version>
    </dependency>
    

    2.1 JWT整合

    2.1.1 JWT工具类

    JWT工具类起码要具有以下功能:

    • 根据用户信息生成JWT;
    • 校验JWT是否合法,如是否被篡改、是否过期等;
    • 从JWT中解析用户信息,如用户名、权限等;

    具体代码如下:

    @Component
    public class JwtTokenProvider {
    
        @Autowired JwtProperties jwtProperties;
    
        @Autowired
        private CustomUserDetailsService userDetailsService;
    
        private String secretKey;
    
        @PostConstruct
        protected void init() {
            secretKey = Base64.getEncoder().encodeToString(jwtProperties.getSecretKey().getBytes());
        }
    
        public String createToken(String username, List<String> roles) {
    
            Claims claims = Jwts.claims().setSubject(username);
            claims.put("roles", roles);
    
            Date now = new Date();
            Date validity = new Date(now.getTime() + jwtProperties.getValidityInMs());
    
            return Jwts.builder()//
                    .setClaims(claims)//
                    .setIssuedAt(now)//
                    .setExpiration(validity)//
                    .signWith(SignatureAlgorithm.HS256, secretKey)//
                    .compact();
        }
    
        public Authentication getAuthentication(String token) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(getUsername(token));
            return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
        }
    
        public String getUsername(String token) {
            return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
        }
    
        public String resolveToken(HttpServletRequest req) {
            String bearerToken = req.getHeader("Authorization");
            if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
                return bearerToken.substring(7);
            }
            return null;
        }
    
        public boolean validateToken(String token) {
            try {
                Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
    
                if (claims.getBody().getExpiration().before(new Date())) {
                    return false;
                }
    
                return true;
            } catch (JwtException | IllegalArgumentException e) {
                throw new InvalidJwtAuthenticationException("Expired or invalid JWT token");
            }
        }
    
    }
    

    工具类还实现了另一个功能:从HTTP请求头中获取JWT

    2.1.2 Token处理的Filter

    FilterSecurity处理的关键,基本上都是通过Filter来拦截请求的。首先从请求头取出JWT,然后校验JWT是否合法,如果合法则取出Authentication保存在SecurityContextHolder里。如果不合法,则做异常处理。

    public class JwtTokenAuthenticationFilter extends GenericFilterBean {
    
        private JwtTokenProvider jwtTokenProvider;
    
        public JwtTokenAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
            this.jwtTokenProvider = jwtTokenProvider;
        }
    
        @Override
        public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain)
                throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) res;
    
            try {
                String token = jwtTokenProvider.resolveToken(request);
                if (token != null && jwtTokenProvider.validateToken(token)) {
                    Authentication auth = jwtTokenProvider.getAuthentication(token);
    
                    if (auth != null) {
                        SecurityContextHolder.getContext().setAuthentication(auth);
                    }
                }
            } catch (InvalidJwtAuthenticationException e) {
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                response.getWriter().write("Invalid token");
                response.getWriter().flush();
                return;
            }
    
            filterChain.doFilter(req, res);
        }
    }
    

    对于异常处理,使用@ControllerAdvice是不行的,应该这个是Filter,在这里抛的异常还没有到DispatcherServlet,无法处理。所以Filter要自己做异常处理:

    catch (InvalidJwtAuthenticationException e) {
      response.setStatus(HttpStatus.UNAUTHORIZED.value());
      response.getWriter().write("Invalid token");
      response.getWriter().flush();
      return;
    }
    

    最后的return;不能省略,因为已经把要输出的内容给Response了,没有必要再往后传递,否则会报错:

    java.lang.IllegalStateException: getWriter() has already been called
    

    2.1.3 JWT属性

    JWT需要配置一个密钥来加密,同时还要配置JWT令牌的有效期。

    @Configuration
    @ConfigurationProperties(prefix = "pkslow.jwt")
    public class JwtProperties {
        private String secretKey = "pkslow.key";
        private long validityInMs = 3600_000;
    //getter and setter
    }
    

    2.2 Spring Security整合

    Spring Security的整个框架还是比较复杂的,简化后大概如下图所示:

    它是通过一连串的Filter来进行安全管理。细节这里先不展开讲。

    2.2.1 WebSecurityConfigurerAdapter配置

    这个配置也可以理解为是FilterChain的配置,可以不用理解,代码很好懂它做了什么:

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    
        @Autowired
        JwtTokenProvider jwtTokenProvider;
    
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return NoOpPasswordEncoder.getInstance();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .httpBasic().disable()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/auth/login").permitAll()
                .antMatchers(HttpMethod.GET, "/admin").hasRole("ADMIN")
                .antMatchers(HttpMethod.GET, "/user").hasRole("USER")
                .anyRequest().authenticated()
                .and()
                .apply(new JwtSecurityConfigurer(jwtTokenProvider));
        }
    }
    

    这里通过HttpSecurity配置了哪些请求需要什么权限才可以访问。

    • /auth/login用于登陆获取JWT,所以都能访问;
    • /admin只有ADMIN用户才可以访问;
    • /user只有USER用户才可以访问。

    而之前实现的Filter则在下面配置使用:

    public class JwtSecurityConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    
        private JwtTokenProvider jwtTokenProvider;
    
        public JwtSecurityConfigurer(JwtTokenProvider jwtTokenProvider) {
            this.jwtTokenProvider = jwtTokenProvider;
        }
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
            JwtTokenAuthenticationFilter customFilter = new JwtTokenAuthenticationFilter(jwtTokenProvider);
            http.exceptionHandling()
                    .authenticationEntryPoint(new JwtAuthenticationEntryPoint())
                    .and()
                    .addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
        }
    }
    

    2.2.2 用户从哪来

    通常在Spring Security的世界里,都是通过实现UserDetailsService来获取UserDetails的。

    @Component
    public class CustomUserDetailsService implements UserDetailsService {
    
        private UserRepository users;
    
        public CustomUserDetailsService(UserRepository users) {
            this.users = users;
        }
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            return this.users.findByUsername(username)
                    .orElseThrow(() -> new UsernameNotFoundException("Username: " + username + " not found"));
        }
    }
    

    对于UserRepository,可以从数据库中读取,或者其它用户管理中心。为了方便,我使用Map放了两个用户:

    @Repository
    public class UserRepository {
    
        private static final Map<String, User> allUsers = new HashMap<>();
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @PostConstruct
        protected void init() {
            allUsers.put("pkslow", new User("pkslow", passwordEncoder.encode("123456"), Collections.singletonList("ROLE_ADMIN")));
            allUsers.put("user", new User("user", passwordEncoder.encode("123456"), Collections.singletonList("ROLE_USER")));
        }
    
        public Optional<User> findByUsername(String username) {
            return Optional.ofNullable(allUsers.get(username));
        }
    }
    

    3 测试

    完成代码编写后,我们来测试一下:

    (1)无JWT访问,失败

    curl http://localhost:8080/admin
    {"timestamp":"2021-02-06T05:45:06.385+0000","status":403,"error":"Forbidden","message":"Access Denied","path":"/admin"}
    
    $ curl http://localhost:8080/user
    {"timestamp":"2021-02-06T05:45:16.438+0000","status":403,"error":"Forbidden","message":"Access Denied","path":"/user"}
    

    (2)admin获取JWT,密码错误则失败,密码正确则成功

    $ curl http://localhost:8080/auth/login -X POST -d '{"username":"pkslow","password":"xxxxxx"}' -H 'Content-Type: application/json'
    {"timestamp":"2021-02-06T05:47:16.254+0000","status":403,"error":"Forbidden","message":"Access Denied","path":"/auth/login"}
    
    $ curl http://localhost:8080/auth/login -X POST -d '{"username":"pkslow","password":"123456"}' -H 'Content-Type: application/json'
    eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwa3Nsb3ciLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sImlhdCI6MTYxMjU5MDYxNCwiZXhwIjoxNjEyNTkxMjE0fQ.d4Gi50aaOsHHqpM0d8Mh1960otnZf7rlE3x6xSfakVo 
    

    (3)admin带JWT访问/admin,成功;访问/user失败

    $ curl http://localhost:8080/admin -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwa3Nsb3ciLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sImlhdCI6MTYxMjU5MDYxNCwiZXhwIjoxNjEyNTkxMjE0fQ.d4Gi50aaOsHHqpM0d8Mh1960otnZf7rlE3x6xSfakVo'
    you are admin
    
    $ curl http://localhost:8080/user -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwa3Nsb3ciLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sImlhdCI6MTYxMjU5MDYxNCwiZXhwIjoxNjEyNTkxMjE0fQ.d4Gi50aaOsHHqpM0d8Mh1960otnZf7rlE3x6xSfakVo'
    {"timestamp":"2021-02-06T05:51:23.099+0000","status":403,"error":"Forbidden","message":"Forbidden","path":"/user"}
    
    

    (4)使用过期的JWT访问,失败

    $ curl http://localhost:8080/admin -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwa3Nsb3ciLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sImlhdCI6MTYxMjU5MDQ0OSwiZXhwIjoxNjEyNTkwNTA5fQ.CSaubE4iJcYATbLmbb59aNFU1jNCwDFHUV3zIakPU64'
    Invalid token
    

    对于用户user同样可以测试,这里不列出来了。

    4 总结

    代码请查看:https://github.com/LarryDpk/pkslow-samples


    欢迎关注微信公众号<南瓜慢说>,将持续为你更新...

    多读书,多分享;多写作,多整理。

  • 相关阅读:
    tensorflow2.0 GPU和CPU 时间对比
    第一次使用FileZilla Server
    PremiumSoft Navicat 15 for Oracle中文破解版安装教程
    Unmapped Spring configuration files found. Please configure Spring facet or use 'Create Default Context' to add one including all unmapped files.
    ng : 无法加载文件 D: odejs ode_global g.ps1,因为在此系统上禁止运行脚本。有关详细信息,请参阅 https:/go.microsoft.com/fwlink/?LinkID=135170 中的 about_Execution_Policies。
    angular
    Github上优秀的go项目
    win10---file explore 中remove quick access folder
    react--useEffect使用
    linux---cat 和 grep 的妙用
  • 原文地址:https://www.cnblogs.com/larrydpk/p/14939748.html
Copyright © 2011-2022 走看看