Spring Boot Security 整合 JWT 授权 RestAPI 使用 io.jsonwebtoken:jjwt 版本

yufei       4 年, 6 月 前       1351

这几天学习 Spring Boot,想用 JWT 来实现登录 token ,掉进很多坑,终于是完成基本的使用能用了.

目录结构

首先完成后的项目的目录结构如下

.
├── build.gradle
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── my
    │   │       └── demo
    │   │           ├── DemoApplication.java
    │   │           ├── component
    │   │           │   └── JwtTokenComponent.java
    │   │           ├── config
    │   │           │   ├── JwtTokenConfig.java
    │   │           │   └── JwtUserDetailsService.java
    │   │           ├── filters
    │   │           │   └── JwtTokenFilter.java
    │   │           └── web
    │   │               ├── controllers
    │   │               │   └── JwtController.java
    │   │               └── params
    │   │                   └── JwtParam.java
    │   └── resources
    │       ├── application.properties
    │       ├── static
    │       └── templates
    └── test
        └── java
            └── my
                └── demo
                    └── DemoApplicationTests.java

文件说明

  1. 我们使用 Gradle 来管理依赖,来看看项目完成后的 build.gradle 里的内容

    plugins {
        id 'org.springframework.boot' version '2.2.6.RELEASE'
        id 'io.spring.dependency-management' version '1.0.9.RELEASE'
        id 'java'
    }
    
    group = 'my'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '1.8'
    
    repositories {
        jcenter()
        mavenCentral()
    }
    
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-security'
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'io.jsonwebtoken:jjwt:0.9.1'
        implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.0'
        implementation 'javax.xml.bind:jaxb-api:2.3.1'
    
        compileOnly 'org.projectlombok:lombok'
        annotationProcessor 'org.projectlombok:lombok'
    
        testImplementation('org.springframework.boot:spring-boot-starter-test') {
            exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
        }
        testImplementation 'org.springframework.security:spring-security-test'
    }
    
    test {
        useJUnitPlatform()
    }
    
  2. 既然使用了 JWT,那么我们首先需要一个类或者工具,用来 生成 JWT 令牌,检验 JWT 令牌,从令牌中获取信息

    在 Spring Boot 中,一般使用 组件 Component 来实现。

    首先创建一个包

    my.demo.component
    

    然后添加 JwtTokenComponent 类,内容如下

    package my.demo.component;
    
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.stereotype.Component;
    
    import java.io.Serializable;
    import java.time.Instant;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    
    @Component
    public class JwtTokenComponent implements Serializable {
    
        private static final String CLAIM_KEY_USERNAME = "sub";
    
        private static final long EXPIRATION_TIME = 12 * 3600 * 1000;
    
        private static final String SECRET = "167e5226-20ec-47e6-8cd7-0e9074490d52";
    
        public String generateToken(UserDetails userDetails) {
            Map<String,Object> claims = new HashMap<>(16);
    
            claims.put(CLAIM_KEY_USERNAME,userDetails.getUsername());
            return Jwts.builder()
                    .setClaims(claims)
                    .setExpiration(new Date(Instant.now().toEpochMilli() + EXPIRATION_TIME))
                    .signWith(SignatureAlgorithm.HS512,SECRET)
                    .compact();
        }
    
        public Boolean validateToken(String token, UserDetails userDetails) {
            User user = (User) userDetails;
            String username = getUsernameFromToken(token);
            return (username.equals(user.getUsername())) && ! isTokenExpired(token);
        }
    
        private Claims getClaimsFromToken(String token) {
            return Jwts.parser()
                    .setSigningKey(SECRET)
                    .parseClaimsJws(token)
                    .getBody();
        }
    
        public String getUsernameFromToken(String token) {
            return getClaimsFromToken(token).getSubject();
        }
    
        public Date getExpirationDateFromToken(String token) {
            return getClaimsFromToken(token).getExpiration();
        }
    
        public Boolean isTokenExpired(String token) {
            Date expiration = getExpirationDateFromToken(token);
            return expiration.before(new Date());
        }
    }
    
  3. 其次,我们需要自定义 用户服务 UserDetailsService ,用来实现如何加载 用户

    因为只想体验 JWT,并不想实现一套完整的数据库,也就是我们的用户要硬编码在文件里

    首先创建一个包

    my.demo.config
    

    然后添加一个类 JwtUserDetailsService 实现 UserDetailsService 接口,其实就是自定义 loadUserByUsername() 方法

    package my.demo.config;
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
    
    import java.util.ArrayList;
    
    @Service
    public class JwtUserDetailsService implements UserDetailsService {
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            if(username.equals("admin")) {
                return new User(
                        "admin","$2a$10$slYQmyNdGzTn7ZLBXBChFOC9f6kFjAqPhccnP6DxlWXx2lPk1C3G6", new ArrayList<>()
                );
            }
    
            return null;
        }
    }
    
  4. 接下来我们要做的,就是从 HTTP 请求头里获取 token 令牌,然后解析令牌,创建用户,设置验证用户。

    这些操作,Spring Boot 中我们一般通过 过滤器 来实现。

    首先创建包

    my.demo.filters
    

    然后创建一个类 JwtTokenFilter 实现 OncePerRequestFilter 接口重写 doFilterInternal() 方法

    为什么是 OncePerRequestFilter 因为我们每次请求都是要重新验证的

    package my.demo.filters;
    
    import my.demo.component.JwtTokenComponent;
    import my.demo.config.JwtUserDetailsService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
    import org.springframework.stereotype.Component;
    import org.springframework.web.filter.OncePerRequestFilter;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    @Component
    public class JwtTokenFilter extends OncePerRequestFilter {
    
        public static final String JWT_TOKEN_HEADER = "Authorization";
    
        @Autowired
        private JwtUserDetailsService userDetailsService;
    
        @Autowired
        private JwtTokenComponent  jwtTokenComponent;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            String token = request.getHeader(JWT_TOKEN_HEADER);
    
            if(null != token) {
                String username = jwtTokenComponent.getUsernameFromToken(token);
                if(username != null && SecurityContextHolder
                        .getContext().getAuthentication() == null) {
                    UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                    if(jwtTokenComponent.validateToken(token,userDetails)) {
                        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities()
                        );
                        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
    
                        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                    }
                }
            }
    
            filterChain.doFilter(request,response);
        }
    }
    
  5. 已经实现了过滤器,那么现在我们要做的就是添加配置,告诉 Spring Boot 哪些路径要验证,哪些路径不要验证

    我们在 my.demo.config 目录下新建一个类 JwtTokenConfig 继承自 WebSecurityConfigurerAdapter 然后实现 configure() 方法

    package my.demo.config;
    
    import my.demo.filters.JwtTokenFilter;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.HttpMethod;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    
    @Configuration
    @EnableWebSecurity
    public class JwtTokenConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        public  JwtUserDetailsService userDetailsService;
    
        @Bean
        public JwtTokenFilter authenticationTokenFilterBean() {
            return new JwtTokenFilter();
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Bean
        @Override
        public AuthenticationManager  authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        @Autowired
        public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
        }
    
        @Override
        protected  void configure(HttpSecurity httpSecurity) throws Exception {
            httpSecurity.cors().and().csrf().disable()
                    .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .authorizeRequests()
                    .antMatchers(HttpMethod.OPTIONS,"/**").permitAll()
                    .antMatchers("/auth/login").permitAll()
                    .anyRequest().authenticated();
            httpSecurity.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
            httpSecurity.headers().cacheControl();
        }
    }
    
  6. 好了,准备工作已经完成,我们接下来要实现控制器了。

    在上一个步骤中,我们的登录路径是 /auth/login,然后我们实现 /auth/hello 需要登录后才能访问

    首先我们创建一个接受请求参数的类 JwtParam,这个类在 my.demo.web.params 中,内容如下

    package my.demo.web.params;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import lombok.ToString;
    
    @Data
    @ToString
    @AllArgsConstructor
    @NoArgsConstructor
    public class JwtParam {
        private String username;
        private String password;
    }
    

然后我们实现一个控制器 JwtController 这个类在 my.demo.web.controllers 包下,内容如下

    package my.demo.web.controllers;

    import my.demo.component.JwtTokenComponent;
    import my.demo.config.JwtUserDetailsService;
    import my.demo.web.params.JwtParam;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.web.bind.annotation.*;

    import javax.security.sasl.AuthenticationException;

    @RestController
    @RequestMapping(value =  "auth")
    public class JwtController {
        @Autowired
        private JwtTokenComponent jwtTokenComponent;

        @Autowired
        private AuthenticationManager authenticationManager;

        @Autowired
        private JwtUserDetailsService jwtUserDetailsService;

        @PostMapping(value="login")
        public String login(@RequestBody JwtParam  body) throws AuthenticationException {
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                    body.getUsername(),body.getPassword()
            );

            Authentication authentication = authenticationManager.authenticate(authenticationToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(body.getUsername());

            return jwtTokenComponent.generateToken(userDetails);
        }

        @GetMapping(value="hello")
        public String hello() {
            return "Hello Jwt!";
        }
    }

运行

好啦,代码工作终于完成了,我们可以使用 gradle bootRun 运行起来了

  1. 检查没登录时的授权

    curl -X GET 'http://localhost:8080/auth/hello'
    

    会出现以下错误信息

    {"timestamp":"2020-05-08T00:55:18.179+00:00","status":403,"error":"Forbidden","message":"Access Denied","path":"/auth/hello"}
    

    提示我们未授权,代表拦截器配置正确了。

  2. 然后我们使用 /auth/login 去获得授权的 token

    curl -X POST 'http://127.0.0.1:8080/auth/login' --header 'Content-Type: application/json' -d '{"username": "admin", "password": "password"}'
    

    返回以下token信息

    eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTU4ODg5OTUxN30._Kkhe5OMJapmQijB050lPbv4ea1wdumgCVH7-_IfrUMcj1YOKPvHuaoMQVeQOtG6ehy-ty7jPRFuMYelN_j2cA
    
  3. 然后我们使用获取的 token 信息来获取接口数据

    curl -X GET 'http://127.0.0.1:8080/auth/hello' --header 'Authorization: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTU4ODg5OTUxN30._Kkhe5OMJapmQijB050lPbv4ea1wdumgCVH7-_IfrUMcj1YOKPvHuaoMQVeQOtG6ehy-ty7jPRFuMYelN_j2cA'
    

    返回以下信息

    Hello Jwt!!!
    

成功。这就完成了使用 JWT 的所有步骤

目前尚无回复
简单教程 = 简单教程,简单编程
简单教程 是一个关于技术和学习的地方
现在注册
已注册用户请 登入
关于   |   FAQ   |   我们的愿景   |   广告投放   |  博客

  简单教程,简单编程 - IT 入门首选站

Copyright © 2013-2022 简单教程 twle.cn All Rights Reserved.