相關介紹
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