一、引言
在分布式系统和前后端分离架构中,传统的基于 session 的认证方式存在跨域难处理、服务端存储压力大等问题。jwt(json web token) 作为一种轻量级的身份认证与授权方案,凭借其无状态、可跨域、易于扩展的特性,成为 spring boot 项目中实现认证授权的主流选择。本文将从环境搭建、核心实现到进阶优化,完整讲解 spring boot 整合 jwt 实现登录认证与接口授权的全流程。
二、技术栈与环境准备
1. 核心依赖
在 spring boot 项目的pom.xml(maven)或build.gradle(gradle)中引入以下
- spring security:提供认证与授权的基础框架
- jjwt:java 领域主流的 jwt 工具库,支持 jwt 的生成、解析与验证
- spring web:用于编写接口测试认证授权流程
maven 依赖配置示例:
<!-- spring boot starter web -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
</dependency>
<!-- spring security -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-security</artifactid>
</dependency>
<!-- jwt -->
<dependency>
<groupid>io.jsonwebtoken</groupid>
<artifactid>jjwt-api</artifactid>
<version>0.11.5</version>
</dependency>
<dependency>
<groupid>io.jsonwebtoken</groupid>
<artifactid>jjwt-impl</artifactid>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupid>io.jsonwebtoken</groupid>
<artifactid>jjwt-jackson</artifactid>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
2. 核心概念说明
jwt 结构: 由 header(头部)、payload(载荷)、signature(签名)三部分组成,以.分隔,例如eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyjzdwiioiixmjm0nty3odkwiiwibmftzsi6ikpvag4grg9liiwiawf0ijoxnte2mjm5mdiyfq.sflkxwrjsmekkf2qt4fwpmejf36pok6yjv_adqssw5c
- header:存储算法类型和令牌类型,如{"alg":"hs256","typ":"jwt"}
- payload:存储用户身份信息(如用户名、角色)和过期时间等声明,不建议存放敏感信息
- signature:通过 header 指定的算法,结合密钥对 header 和 payload 进行加密,保证令牌不被篡改
认证流程: 用户登录成功后,服务端生成 jwt 返回给客户端;客户端后续请求携带 jwt,服务端验证令牌有效性后完成授权
三、核心功能实现
1. jwt 工具类封装
@component
public class jwtutils {
@value("${jwt.secret}")
private string secret;
@value("${jwt.access-token-expire-time}")
private long accesstokenexpiretime;
@value("${jwt.refresh-token-expire-time}")
private long refreshtokenexpiretime;
/**
* 生成访问令牌
*/
public string generateaccesstoken(string username) {
map<string, object> claims = new hashmap<>();
return generatetoken(claims, username, accesstokenexpiretime);
}
/**
* 生成刷新令牌
*/
public string generaterefreshtoken(string username) {
map<string, object> claims = new hashmap<>();
return generatetoken(claims, username, refreshtokenexpiretime);
}
/**
* 生成token
*/
private string generatetoken(map<string, object> claims, string subject, long expiretime) {
secretkey key = keys.hmacshakeyfor(secret.getbytes(standardcharsets.utf_8));
return jwts.builder()
.setclaims(claims)
.setsubject(subject)
.setissuedat(new date())
.setexpiration(new date(system.currenttimemillis() + expiretime))
.signwith(key, signaturealgorithm.hs512)
.compact();
}
/**
* 从token中获取用户名
*/
public string getusernamefromtoken(string token) {
claims claims = getclaimsfromtoken(token);
return claims.getsubject();
}
/**
* 验证token是否有效
*/
public boolean validatetoken(string token) {
try {
secretkey key = keys.hmacshakeyfor(secret.getbytes(standardcharsets.utf_8));
jwts.parserbuilder()
.setsigningkey(key)
.build()
.parseclaimsjws(token);
return true;
} catch (exception e) {
return false;
}
}
/**
* 判断token是否过期
*/
public boolean istokenexpired(string token) {
claims claims = getclaimsfromtoken(token);
date expiration = claims.getexpiration();
return expiration.before(new date());
}
/**
* 从token中获取claims
*/
private claims getclaimsfromtoken(string token) {
secretkey key = keys.hmacshakeyfor(secret.getbytes(standardcharsets.utf_8));
return jwts.parserbuilder()
.setsigningkey(key)
.build()
.parseclaimsjws(token)
.getbody();
}
}
2. 实现认证过滤器
@component
public class jwtauthenticationfilter extends onceperrequestfilter {
@autowired
private jwtutils jwtutils;
@autowired
private userdetailsservice userdetailsservice;
@override
protected void dofilterinternal(httpservletrequest request, httpservletresponse response, filterchain chain)
throws servletexception, ioexception {
// 跳过登录和刷新令牌的接口
string requesturi = request.getrequesturi();
if (requesturi.equals("/api/auth/login") || requesturi.equals("/api/auth/refresh")) {
chain.dofilter(request, response);
return;
}
// 从请求头获取token
string token = gettokenfromrequest(request);
if (stringutils.hastext(token) && jwtutils.validatetoken(token)) {
string username = jwtutils.getusernamefromtoken(token);
userdetails userdetails = userdetailsservice.loaduserbyusername(username);
// 设置认证信息到上下文
usernamepasswordauthenticationtoken authentication = new usernamepasswordauthenticationtoken(
userdetails, null, userdetails.getauthorities());
authentication.setdetails(new webauthenticationdetailssource().builddetails(request));
securitycontextholder.getcontext().setauthentication(authentication);
} else {
securitycontextholder.clearcontext();
}
chain.dofilter(request, response);
}
private string gettokenfromrequest(httpservletrequest request) {
string bearertoken = request.getheader("authorization");
if (stringutils.hastext(bearertoken) && bearertoken.startswith("bearer ")) {
return bearertoken.substring(7);
}
return null;
}
}
3. 实现无感刷新令牌过滤器
创建jwtrefreshfilter实现令牌的无感刷新:
@component
public class jwtrefreshfilter extends onceperrequestfilter {
@autowired
private jwtutils jwtutils;
@autowired
private userdetailsservice userdetailsservice;
// 当token剩余有效期小于10分钟时,自动刷新
private static final long refresh_threshold = 10 * 60 * 1000;
@override
protected void dofilterinternal(httpservletrequest request, httpservletresponse response, filterchain filterchain)
throws servletexception, ioexception {
// 跳过登录和刷新接口
string requesturi = request.getrequesturi();
if (requesturi.equals("/api/auth/login") || requesturi.equals("/api/auth/refresh")) {
filterchain.dofilter(request, response);
return;
}
// 获取请求头中的token
string authorizationheader = request.getheader("authorization");
if (authorizationheader == null || !authorizationheader.startswith("bearer ")) {
filterchain.dofilter(request, response);
return;
}
string token = authorizationheader.substring(7);
try {
if (jwtutils.validatetoken(token)) {
// 检查token是否即将过期
long remainingtime = jwtutils.gettokenremainingtime(token);
if (remainingtime < refresh_threshold) {
string username = jwtutils.getusernamefromtoken(token);
string newaccesstoken = jwtutils.generateaccesstoken(username);
// 将新token添加到响应头
response.setheader("authorization", "bearer " + newaccesstoken);
}
}
} catch (exception e) {
securitycontextholder.clearcontext();
}
filterchain.dofilter(request, response);
}
}
4. 配置 spring security
创建securityconfig配置安全规则:
@configuration
@enablewebsecurity
@enablemethodsecurity(prepostenabled = true)
public class securityconfig {
@bean
public passwordencoder passwordencoder() {
return new bcryptpasswordencoder();
}
@bean
public authenticationmanager authenticationmanager(authenticationconfiguration authenticationconfiguration) throws exception {
return authenticationconfiguration.getauthenticationmanager();
}
@bean
public securityfilterchain securityfilterchain(httpsecurity http, jwtauthenticationfilter jwtauthfilter,
jwtrefreshfilter jwtrefreshfilter) throws exception {
http
.cors().and().csrf().disable()
.sessionmanagement().sessioncreationpolicy(sessioncreationpolicy.stateless)
.and()
.authorizehttprequests()
.antmatchers("/api/auth/**", "/api/public/**").permitall()
.anyrequest().authenticated()
.and()
.exceptionhandling()
.authenticationentrypoint((request, response, ex) -> {
response.setstatus(httpservletresponse.sc_unauthorized);
response.setcontenttype("application/json");
response.getwriter().write("{\"error\":\"unauthorized\",\"message\":\"authentication required\"}");
});
// 添加jwt过滤器
http.addfilterbefore(jwtauthfilter, usernamepasswordauthenticationfilter.class);
http.addfilterbefore(jwtrefreshfilter, usernamepasswordauthenticationfilter.class);
return http.build();
}
}
5. 实现认证服务
创建authservice接口和实现类处理登录和刷新令牌业务:
public interface authservice {
loginresponsedto login(loginrequestdto loginrequest);
loginresponsedto refreshtoken(refreshtokenrequestdto refreshtokenrequest);
}
@service
public class authserviceimpl implements authservice {
@autowired
private authenticationmanager authenticationmanager;
@autowired
private jwtutils jwtutils;
@autowired
private stringredistemplate stringredistemplate;
@value("${jwt.refresh-token-expire-time}")
private long refreshtokenexpiretime;
@override
public loginresponsedto login(loginrequestdto loginrequest) {
// 验证输入
if (loginrequest == null || stringutils.isempty(loginrequest.getusername()) ||
stringutils.isempty(loginrequest.getpassword())) {
throw new badcredentialsexception("invalid login request");
}
try {
// 执行认证
authentication authentication = authenticationmanager.authenticate(
new usernamepasswordauthenticationtoken(
loginrequest.getusername(),
loginrequest.getpassword()
)
);
securitycontextholder.getcontext().setauthentication(authentication);
// 生成token
string username = loginrequest.getusername();
string accesstoken = jwtutils.generateaccesstoken(username);
string refreshtoken = jwtutils.generaterefreshtoken(username);
// 存储refresh token到redis
string refreshtokenkey = "refresh_token:" + username;
stringredistemplate.opsforvalue().set(
refreshtokenkey,
refreshtoken,
refreshtokenexpiretime,
timeunit.milliseconds
);
// 构建响应
loginresponsedto response = new loginresponsedto();
response.setaccesstoken(accesstoken);
response.setrefreshtoken(refreshtoken);
response.setexpiresin(refreshtokenexpiretime);
return response;
} catch (authenticationexception e) {
throw new badcredentialsexception("invalid username or password");
}
}
@override
public loginresponsedto refreshtoken(refreshtokenrequestdto refreshtokenrequest) {
// 验证输入
if (refreshtokenrequest == null || stringutils.isempty(refreshtokenrequest.getrefreshtoken())) {
throw new badcredentialsexception("invalid refresh token request");
}
string refreshtoken = refreshtokenrequest.getrefreshtoken();
// 验证refresh token
if (!jwtutils.validatetoken(refreshtoken)) {
throw new badcredentialsexception("invalid refresh token");
}
string username = jwtutils.getusernamefromtoken(refreshtoken);
if (stringutils.isempty(username)) {
throw new badcredentialsexception("invalid refresh token");
}
// 验证redis中存储的refresh token
string storedtoken = stringredistemplate.opsforvalue().get("refresh_token:" + username);
if (storedtoken == null || !storedtoken.equals(refreshtoken)) {
throw new badcredentialsexception("invalid refresh token");
}
// 生成新的token
string newaccesstoken = jwtutils.generateaccesstoken(username);
string newrefreshtoken = jwtutils.generaterefreshtoken(username);
// 更新redis中的refresh token
stringredistemplate.opsforvalue().set(
"refresh_token:" + username,
newrefreshtoken,
refreshtokenexpiretime,
timeunit.milliseconds
);
loginresponsedto response = new loginresponsedto();
response.setaccesstoken(newaccesstoken);
response.setrefreshtoken(newrefreshtoken);
response.setexpiresin(refreshtokenexpiretime);
return response;
}
}
四、相关注解总结
| 注解名称 | 核心作用 | 使用场景 | 关键注意事项 |
|---|---|---|---|
| @enablewebsecurity | 开启 spring security 的 web 安全功能,加载安全过滤器链和相关配置 | 标注在自定义的 spring security 配置类上 | 必须搭配@configuration使用,否则配置不生效 |
@enableglobalmethodsecurity | 开启方法级权限控制,支持多种权限注解 | 标注在 security 配置类上,需指定启用的注解类型 | 常用属性:prepostenabled=true(启用@preauthorize等)、securedenabled=true(启用@secured) |
@preauthorize | 方法执行前校验权限,支持 spel 表达式,可实现复杂权限判断 | 控制器接口方法、服务层方法的权限控制(细粒度权限) | 依赖@enableglobalmethodsecurity(prepostenabled=true),支持角色、权限、请求参数校验 |
@postauthorize | 方法执行后校验权限,基于方法返回值判断 | 需根据返回结果控制权限的场景(极少使用,避免方法执行产生副作用) | spel 表达式中用returnobject指代方法返回值 |
@secured | 基于角色的粗粒度权限控制 | 控制器或服务层方法的角色校验 | 需启用securedenabled=true,角色名称必须以role_为前缀 |
@rolesallowed | jsr-250 规范注解,基于角色的权限控制 | 控制器或服务层方法的多角色授权 | 需启用jsr250enabled=true,角色名称可省略role_前缀(框架自动补充) |
@authenticationprincipal | 直接获取当前认证用户的userdetails或自定义用户信息 | 控制器方法参数中,需获取当前登录用户信息时 | 无需手动从securitycontextholder获取,直接注入即可 |
@prefilter | 方法执行前,对集合类型参数进行过滤,仅保留符合权限的元素 | 数据查询前的参数过滤(数据级权限控制) | 仅对集合类型参数生效,spel 表达式用filterobject指代集合元素 |
@postfilter | 方法执行后,对集合类型返回值进行过滤,仅保留符合权限的元素 | 数据返回后的结果过滤(数据级权限控制) | 仅对集合类型返回值生效,依赖@enableglobalmethodsecurity启用 |
总结
本指南介绍了如何在 spring boot 应用中实现基于 jwt 的认证授权功能,包括:
- jwt 令牌的生成与验证
- 基于 spring security 的认证过滤器
- 令牌刷新机制与无感刷新实现
- 完整的登录与授权流程
通过这种方式,我们可以实现无状态的认证系统,适合分布式应用和前后端分离架构。实际应用中,还可以根据需要添加更多功能,如令牌撤销、角色权限控制等。
以上就是springboot整合jwt实现登录认证与接口授权的全流程的详细内容,更多关于springboot jwt登录认证与接口授权的资料请关注代码网其它相关文章!
发表评论