相关介绍
1)JWT
JWT:全称 JSON Web Token,是一个分布式身份校验方案,可生产 token,也可解析 token
JWT生成的 token 由三部分组成:
- 头部:主要设置一些规范信息,签名部分的编码格式就在头部声明
- 载荷:token 中存放有效信息的部分。比如用户名、角色、过期时间等,切记不要放密码,会泄露
- 签名:将头部与载荷分别采用base64编码后,用“.”相连,再加入盐,最后使用头部声明的编码类型进行编码,就得到了签名。
2)RSA 非对称加密
基本原理:同时生成两把密钥(公钥和私钥),私钥隐秘保存,公钥可发放给信任的客户端
- 私钥加密:持有私钥或公钥可解密
- 公钥加密:持有私钥才可解密
优点:安全,难以破解
缺点:算法耗时,但可以接受
3)项目说明
![]() |
![]() |
![]() |
以下只贴关键代码,需要完整代码见本文末尾 github 链接
公共模块
1)首先创建一个父工程,把其中 src 目录删除,添加 pom 依赖,主要添加 springboot 依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
2)在该工程下创建子模块----公共模块
该模块放了一些 JWT 和 RSA 的工具类,因此需要加入 jwt 和 json 相关的依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springsecurity_jwt_parent</artifactId>
<groupId>com.xiao</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common_module</artifactId>
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.1</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.1</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
</project>
具体工具类见源码>>源码
认证模块
1)认证模块,主要对用户信息进行校验并生成 token。也算是一个完整的项目,需要实体类(用户信息、角色信息)、service 、mapper等。
这些类的代码可以参考《Spring Security 简单认证与授权》
2)配置文件
rsa:
key:
private-key-path: G:\temp\authRsa\rsa_id_key
public-key-path: G:\temp\authRsa\rsa_id_key.pub
这个密钥文件可以通过工具类的 密钥生成方法生成,代码在公共模块的测试类中
3)JWT 的认证和验证 过滤器
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
//过滤器不放到容器中,无法直接注入,需通过构造器传入
private AuthenticationManager authenticationManager;
private RsaKeyProperties rsaKeyProperties;
public JwtLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties rsaKeyProperties){
this.authenticationManager = authenticationManager;
this.rsaKeyProperties = rsaKeyProperties;
}
//认证逻辑
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
try {
//从请求中获取用户信息
SysUser sysUser = new ObjectMapper().readValue(request.getInputStream(), SysUser.class);
UsernamePasswordAuthenticationToken authRequest =
new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword());
return authenticationManager.authenticate(authRequest);
} catch (IOException e) {
//认证失败,返回失败信息
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
try {
PrintWriter writer = response.getWriter();
Map resultMap = new HashMap<>();
resultMap.put("code",HttpServletResponse.SC_UNAUTHORIZED);
resultMap.put("msg", "用户名或密码错误!");
writer.write(new ObjectMapper().writeValueAsString(resultMap));
writer.flush();
writer.close();
}catch(Exception ex) {
ex.printStackTrace();
}
throw new RuntimeException(e);
}
}
// 认证成功后,生成 token,返回 token
public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
SysUser sysUser = new SysUser();
sysUser.setUsername(authResult.getName()); // 密码是敏感信息,不放到头部
sysUser.setRoles((List<SysRole>) authResult.getAuthorities());
String token = JwtUtils.generateTokenExpireInMinutes(sysUser, rsaKeyProperties.getPrivateKey(), 24 * 60);
response.addHeader("Authorization", "Bearer " + token);
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
try {
PrintWriter writer = response.getWriter();
Map resultMap = new HashMap<>();
resultMap.put("code",HttpServletResponse.SC_OK);
resultMap.put("msg", "认证通过!");
writer.write(new ObjectMapper().writeValueAsString(resultMap));
writer.flush();
writer.close();
}catch(Exception ex) {
ex.printStackTrace();
}
}
}
//验证token是否正确
public class JwtVerifyFilter extends BasicAuthenticationFilter {
private RsaKeyProperties rsaKeyProperties;
public JwtVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties rsaKeyProperties) {
super(authenticationManager);
this.rsaKeyProperties = rsaKeyProperties;
}
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader("Authorization");
if(header == null || !header.startsWith("Bearer")){
chain.doFilter(request, response);
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
PrintWriter writer = response.getWriter();
Map resultMap = new HashMap<>();
resultMap.put("code",HttpServletResponse.SC_FORBIDDEN);
resultMap.put("msg", "请登录!");
writer.write(new ObjectMapper().writeValueAsString(resultMap));
writer.flush();
writer.close();
}else {
//携带了正确格式的token
String token = header.replace("Bearer", "");
//验证token是否正确
Payload<SysUser> payload = JwtUtils.getInfoFromToken(token, rsaKeyProperties.getPublicKey(), SysUser.class);
//获取到当前登录用户的信息
SysUser userInfo = payload.getUserInfo();
if(userInfo != null) {
UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken(userInfo.getUsername(), "", userInfo.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
}
}
}
4)配置类主要需要配置 RSA 和 springsecurity
从配置文件绑定属性到实体类中
@ConfigurationProperties(prefix = "rsa.key")
public class RsaPath {
private String publicKeyPath;
private String privateKeyPath;
public String getPublicKeyPath() {
return publicKeyPath;
}
public void setPublicKeyPath(String publicKeyPath) {
this.publicKeyPath = publicKeyPath;
}
public String getPrivateKeyPath() {
return privateKeyPath;
}
public void setPrivateKeyPath(String privateKeyPath) {
this.privateKeyPath = privateKeyPath;
}
}
@Component
public class RsaKeyProperties {
@Autowired
RsaPath rsaPath;
private PublicKey publicKey;
private PrivateKey privateKey;
@PostConstruct //该注释确保publicKeyPath和privateKeyPath都有值后才执行该方法
public void getRsaKey() throws Exception {
System.out.println(rsaPath.getPublicKeyPath());
publicKey = RsaUtils.getPublicKey(rsaPath.getPublicKeyPath());
privateKey = RsaUtils.getPrivateKey(rsaPath.getPrivateKeyPath());
}
public PublicKey getPublicKey() {
return publicKey;
}
public void setPublicKey(PublicKey publicKey) {
this.publicKey = publicKey;
}
public PrivateKey getPrivateKey() {
return privateKey;
}
public void setPrivateKey(PrivateKey privateKey) {
this.privateKey = privateKey;
}
}
@Configuration
@EnableWebSecurity
public class SpringsecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SysUserService userService;
@Autowired
private RsaKeyProperties rsaKeyProperties;
//配置加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); //spring security 内置加密算法
}
//认证用户的来源
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
//Spring Security配置
public void configure(HttpSecurity hs) throws Exception {
hs.csrf()
.disable()
.authorizeRequests()
.antMatchers("/**").hasAnyRole("NORMAL")
.anyRequest().authenticated()
.and() //绑定过滤器
.addFilter(new JwtLoginFilter(super.authenticationManager(), rsaKeyProperties))
.addFilter(new JwtVerifyFilter(super.authenticationManager(), rsaKeyProperties))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //分布式不需要session
}
}
5)测试
使用 postman 进行测试(用到的数据库数据可参考上一篇《Spring Security 简单认证与授权》)
登录认证 > 携带返回的 token 访问资源
资源模块
该模块大部分内容与认证模块相同。该模块不能放置私钥,所以需要把跟私钥相关的信息删除即可。跟认证的逻辑有关的内容也要删除,因为该模块不需要用户登录,只校验其携带的 token。
该模块主要是需要校验请求携带的 token 是否有效,有效则允许访问资源,否则拒绝访问。
具体代码可参考源码
测试
无需进行登录,把刚刚在认证模块返回的、还未失效的 token 拿过来去请求访问资源即可。
本项目 github 地址:https://github.com/godXiaogf/springsecurity_jwt_parent_idea