这几天学习 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
文件说明
-
我们使用 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() }
-
既然使用了 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()); } }
-
其次,我们需要自定义 用户服务
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; } }
-
接下来我们要做的,就是从 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); } }
-
已经实现了过滤器,那么现在我们要做的就是添加配置,告诉 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(); } }
-
好了,准备工作已经完成,我们接下来要实现控制器了。
在上一个步骤中,我们的登录路径是
/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
运行起来了
-
检查没登录时的授权
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"}
提示我们未授权,代表拦截器配置正确了。
-
然后我们使用
/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
-
然后我们使用获取的 token 信息来获取接口数据
curl -X GET 'http://127.0.0.1:8080/auth/hello' --header 'Authorization: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTU4ODg5OTUxN30._Kkhe5OMJapmQijB050lPbv4ea1wdumgCVH7-_IfrUMcj1YOKPvHuaoMQVeQOtG6ehy-ty7jPRFuMYelN_j2cA'
返回以下信息
Hello Jwt!!!
成功。这就完成了使用 JWT 的所有步骤