一、背景介绍
1.1 为什么使用shiro?
apache shiro 是一个强大且易用的 java 安全框架,提供了认证、授权、加密和会话管理功能。在现代应用开发中,shiro 因其简单性和灵活性而被广泛采用:
- 简单易用:相比 spring security,shiro 的 api 更加直观和简单
- 功能全面:提供认证、授权、会话管理、加密等企业级安全功能
- 轻量级:不依赖任何容器,可以独立运行
- 业界规范:被众多企业采用,有丰富的社区支持和文档
1.2 为什么需要双token?
在原有单token方案基础上引入 access token(访问令牌) 和 refresh token(刷新令牌) 的组合,解决以下问题:
- 安全性:access token 短期有效降低泄露风险,refresh token 独立存储且过期时间长
- 用户体验:自动刷新 access token,用户无感知续期
- 合规性:符合 oauth 2.0 标准流程
二、技术栈组成
| 技术组件 | 作用 | 版本要求 |
|---|---|---|
| springboot | 基础框架 | 3.x |
| apache shiro | 认证和授权核心 | 2.0.0+ |
| hutool-jwt | 令牌生成与验证 | 5.8.24+ |
三、环境准备
3.1 创建 springboot 项目
<!-- pom.xml -->
<dependencies>
<!-- shiro核心依赖 -->
<dependency>
<groupid>org.apache.shiro</groupid>
<artifactid>shiro-core</artifactid>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupid>org.apache.shiro</groupid>
<artifactid>shiro-spring</artifactid>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<exclusions>
<exclusion>
<groupid>org.apache.shiro</groupid>
<artifactid>shiro-core</artifactid>
</exclusion>
<exclusion>
<groupid>org.apache.shiro</groupid>
<artifactid>shiro-web</artifactid>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupid>org.apache.shiro</groupid>
<artifactid>shiro-web</artifactid>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<exclusions>
<exclusion>
<groupid>org.apache.shiro</groupid>
<artifactid>shiro-core</artifactid>
</exclusion>
</exclusions>
</dependency>
<!-- hutool-jwt -->
<dependency>
<groupid>cn.hutool</groupid>
<artifactid>hutool-all</artifactid>
<version>5.8.39</version>
</dependency>
</dependencies>四、核心代码实现
4.1 jwt工具类(jwtutil.java)
import cn.hutool.core.lang.assert;
import cn.hutool.core.util.objectutil;
import cn.hutool.extra.spring.springutil;
import cn.hutool.jwt.jwt;
import cn.hutool.jwt.jwtutil;
import cn.hutool.core.date.datetime;
import org.springframework.data.redis.core.redistemplate;
import java.util.hashmap;
import java.util.map;
import java.util.concurrent.timeunit;
public class jwtutil {
private static final string token_key = "sys:token:";
private static final long access_expire = 1000 * 60 * 15; // 15分钟
/**
* 获取密钥(可选,我这里做的是动态配置的,可以根据需要写死就行)
* @return 密钥
*/
private static byte[] getjwtssecret() {
sysparamsservice sysparamsservice = springutil.getbean(sysparamsservice.class);
string jwtsecret = sysparamsservice.getvalue("jwt.secret", true);
return jwtsecret.getbytes();
}
/**
* 获取刷新token过期时间-单位天(可选,我这里做的是动态配置的,可以根据需要写死就行)
* @return 过期时间-单位天
*/
private static int getrefreshexp() {
sysparamsservice sysparamsservice = springutil.getbean(sysparamsservice.class);
string refreshexp = sysparamsservice.getvalue("jwt.exp", true);
return integer.parseint(refreshexp);
}
// 生成双token
public static map<string, string> generatetokens(long userid,string username) {
map<string, string> tokens = new hashmap<>();
// access token
tokens.put("accesstoken", createtoken(userid,username));
// refresh token
tokens.put("refreshtoken", createrefreshtoken(userid));
return tokens;
}
public static string createtoken(long userid,string username) {
map<string, object> payload = new hashmap<>();
payload.put("userid", userid);
payload.put("username", username);
payload.put("type", "access");
payload.put("exp", new datetime(system.currenttimemillis() + access_expire).gettime());
return jwtutil.createtoken(payload, getjwtssecret());
}
public static string createrefreshtoken(long userid) {
map<string, object> payload = new hashmap<>();
payload.put("userid", userid);
payload.put("type", "refresh");
string refreshtoken = jwtutil.createtoken(payload, getjwtssecret());
redistemplate<string, string> redistemplate = springutil.getbean("redistemplate", redistemplate.class);
redistemplate.opsforvalue().set(token_key+userid, refreshtoken, getrefreshexp(), timeunit.days);
return refreshtoken;
}
// 刷新token
public static void refreshaccesstoken(string refreshtoken) {
assert.istrue(jwtutil.verify(refreshtoken, getjwtssecret()), "非法token错误");
jwt jwt = jwtutil.parsetoken(refreshtoken);
assert.istrue(objectutil.equals("refresh", jwt.getpayload("type")), "非法token错误");
long userid = (long) jwt.getpayload("userid");
redistemplate<string, string> redistemplate = springutil.getbean("redistemplate", redistemplate.class);
string redis_refreshtoken = redistemplate.opsforvalue().get(token_key + userid);
assert.istrue(objectutil.equals(redis_refreshtoken, refreshtoken), "token已过期");
//可选 用于延长缓存时间
long expire = redistemplate.getexpire(token_key + userid, timeunit.days);
if(expire == 0){
redistemplate.expire(token_key + userid, 7, timeunit.days);
}
}
/**
* 从token中获取用户id
* @param refreshtoken jwt token字符串
* @return 用户id
*/
public static long getuseridfromrefreshtoken(string refreshtoken) {
assert.istrue(jwtutil.verify(refreshtoken, getjwtssecret()), "非法token错误");
jwt jwt = jwtutil.parsetoken(refreshtoken);
assert.istrue(objectutil.equals("refresh", jwt.getpayload("type")), "非法token错误");
long userid = (long) jwt.getpayload("userid");
redistemplate<string, string> redistemplate = springutil.getbean("redistemplate", redistemplate.class);
long expire = redistemplate.getexpire(token_key + userid, timeunit.days);
assert.istrue(expire >= 0, "token已过期");
string redis_refreshtoken = redistemplate.opsforvalue().get(token_key + userid);
assert.istrue(objectutil.equals(redis_refreshtoken, refreshtoken), "token已过期");
return (long) jwt.getpayload("userid");
}
/**
* 从token中获取用户id
* @param token jwt token字符串
* @return 用户id
*/
public static long getuseridfromtoken(string token) {
assert.istrue(jwtutil.verify(token, getjwtssecret()), "非法token错误");
jwt jwt = jwtutil.parsetoken(token);
assert.istrue(objectutil.equals(jwt.getpayload("type"),"access"), "非法token错误");
assert.istrue(jwt.getpayload("exp")!=null && jwt.validate(0),"token已失效");
return (long) jwt.getpayload("userid");
}
/**
* 从token中获取用户名
* @param token jwt token字符串
* @return 用户名
*/
public static string getusernamefromtoken(string token) {
assert.istrue(jwtutil.verify(token, getjwtssecret()), "非法token错误");
jwt jwt = jwtutil.parsetoken(token);
assert.istrue(jwt.getpayload("exp")!=null && jwt.validate(0),"token已失效");
return jwt.getpayload("username").tostring();
}
}4.2 shiro配置类(shiroconfig.java)
import java.util.hashmap;
import java.util.linkedhashmap;
import java.util.map;
import org.apache.shiro.mgt.securitymanager;
import org.apache.shiro.session.mgt.sessionmanager;
import org.apache.shiro.spring.lifecyclebeanpostprocessor;
import org.apache.shiro.spring.security.interceptor.authorizationattributesourceadvisor;
import org.apache.shiro.spring.web.shirofilterfactorybean;
import org.apache.shiro.web.config.shirofilterconfiguration;
import org.apache.shiro.web.mgt.defaultwebsecuritymanager;
import org.apache.shiro.web.session.mgt.defaultwebsessionmanager;
import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import jakarta.servlet.filter;
@configuration
public class shiroconfig {
@bean
public defaultwebsessionmanager sessionmanager() {
defaultwebsessionmanager sessionmanager = new defaultwebsessionmanager();
sessionmanager.setsessionvalidationschedulerenabled(false);
sessionmanager.setsessionidurlrewritingenabled(false);
return sessionmanager;
}
@bean("securitymanager")
public securitymanager securitymanager(oauth2realm oauth2realm, sessionmanager sessionmanager) {
defaultwebsecuritymanager securitymanager = new defaultwebsecuritymanager();
securitymanager.setrealm(oauth2realm);
securitymanager.setsessionmanager(sessionmanager);
securitymanager.setremembermemanager(null);
return securitymanager;
}
@bean("shirofilter")
public shirofilterfactorybean shirfilter(securitymanager securitymanager, sysparamsservice sysparamsservice) {
shirofilterconfiguration config = new shirofilterconfiguration();
config.setfilteronceperrequest(true);
shirofilterfactorybean shirofilter = new shirofilterfactorybean();
shirofilter.setsecuritymanager(securitymanager);
shirofilter.setshirofilterconfiguration(config);
map<string, filter> filters = new hashmap<>();
// oauth过滤
filters.put("oauth2", new oauth2filter());
map<string, string> filtermap = new linkedhashmap<>();
filtermap.put("/v3/api-docs/**", "anon");
filtermap.put("/doc.html", "anon");
filtermap.put("/favicon.ico", "anon");
filtermap.put("/refreshtoken", "anon");
filtermap.put("/login", "anon");
filtermap.put("/**", "oauth2");
shirofilter.setfilterchaindefinitionmap(filtermap);
return shirofilter;
}
@bean("lifecyclebeanpostprocessor")
public lifecyclebeanpostprocessor lifecyclebeanpostprocessor() {
return new lifecyclebeanpostprocessor();
}
@bean
public authorizationattributesourceadvisor authorizationattributesourceadvisor(securitymanager securitymanager) {
authorizationattributesourceadvisor advisor = new authorizationattributesourceadvisor();
advisor.setsecuritymanager(securitymanager);
return advisor;
}
}4.3 注册过滤器
@configuration
public class filterconfig {
@bean
public filterregistrationbean<delegatingfilterproxy> shirofilterregistration() {
filterregistrationbean<delegatingfilterproxy> registration = new filterregistrationbean<>();
registration.setfilter(new delegatingfilterproxy("shirofilter"));
// 该值缺省为false,表示生命周期由springapplicationcontext管理,设置为true则表示由servletcontainer管理
registration.addinitparameter("targetfilterlifecycle", "true");
registration.setenabled(true);
registration.setorder(integer.max_value - 1);
registration.addurlpatterns("/*");
return registration;
}
}4.4 注册oauth2过滤器
public class oauth2filter extends authenticatingfilter {
private static final logger logger = loggerfactory.getlogger(oauth2filter.class);
@override
protected authenticationtoken createtoken(servletrequest request, servletresponse response) throws exception {
// 获取请求token
string token = getrequesttoken((httpservletrequest) request);
if (stringutils.isblank(token)) {
logger.warn("createtoken:token is empty");
return null;
}
return new oauth2token(token);
}
@override
protected boolean isaccessallowed(servletrequest request, servletresponse response, object mappedvalue) {
if (((httpservletrequest) request).getmethod().equals(requestmethod.options.name())) {
return true;
}
return false;
}
@override
protected boolean onaccessdenied(servletrequest request, servletresponse response) throws exception {
// 获取请求token,如果token不存在,直接返回401
string token = getrequesttoken((httpservletrequest) request);
if (stringutils.isblank(token)) {
logger.warn("onaccessdenied:token is empty");
httpservletresponse httpresponse = (httpservletresponse) response;
httpresponse.setcontenttype("application/json;charset=utf-8");
httpresponse.setheader("access-control-allow-credentials", "true");
httpresponse.setheader("access-control-allow-origin", httpcontextutils.getorigin());
string json = jsonutils.tojsonstring(new result<void>().error(errorcode.unauthorized));
httpresponse.getwriter().print(json);
return false;
}
return executelogin(request, response);
}
@override
protected boolean onloginfailure(authenticationtoken token, authenticationexception e, servletrequest request,
servletresponse response) {
httpservletresponse httpresponse = (httpservletresponse) response;
httpresponse.setcontenttype("application/json;charset=utf-8");
httpresponse.setheader("access-control-allow-credentials", "true");
httpresponse.setheader("access-control-allow-origin", httpcontextutils.getorigin());
try {
throwable throwable = e.getcause() == null ? e : e.getcause();
result<void> r = new result<void>().error(errorcode.unauthorized, throwable.getmessage());
string json = jsonutils.tojsonstring(r);
httpresponse.getwriter().print(json);
} catch (ioexception e1) {
}
return false;
}
/**
* 获取请求的token
*/
private string getrequesttoken(httpservletrequest httprequest) {
string token = null;
// 从header中获取token
string authorization = httprequest.getheader(constant.authorization);
if (stringutils.isnotblank(authorization) && authorization.startswith("bearer ")) {
token = authorization.replace("bearer ", "");
}
return token;
}
}4.5 认证类
import java.util.hashset;
import java.util.set;
import org.apache.shiro.authc.authenticationexception;
import org.apache.shiro.authc.authenticationinfo;
import org.apache.shiro.authc.authenticationtoken;
import org.apache.shiro.authc.disabledaccountexception;
import org.apache.shiro.authc.incorrectcredentialsexception;
import org.apache.shiro.authc.lockedaccountexception;
import org.apache.shiro.authc.simpleauthenticationinfo;
import org.apache.shiro.authz.authorizationinfo;
import org.apache.shiro.authz.simpleauthorizationinfo;
import org.apache.shiro.realm.authorizingrealm;
import org.apache.shiro.subject.principalcollection;
import org.slf4j.logger;
import org.slf4j.loggerfactory;
import org.springframework.context.annotation.lazy;
import org.springframework.stereotype.component;
import jakarta.annotation.resource;
@component
public class oauth2realm extends authorizingrealm {
@resource
private userservice userservice;
private static final logger logger = loggerfactory.getlogger(oauth2realm.class);
@override
public boolean supports(authenticationtoken token) {
return token instanceof oauth2token;
}
/**
* 授权(验证权限时调用)
*/
@override
protected authorizationinfo dogetauthorizationinfo(principalcollection principals) {
userdetail user = (userdetail) principals.getprimaryprincipal();
// 用户权限列表
set<string> permsset = new hashset<>();
if (user.getsuperadmin() == superadminenum.yes.value()) {
permsset.add("sys:role:superadmin");
permsset.add("sys:role:normal");
} else {
permsset.add("sys:role:normal");
}
simpleauthorizationinfo info = new simpleauthorizationinfo();
info.setstringpermissions(permsset);
return info;
}
/**
* 认证(登录时调用)
*/
@override
protected authenticationinfo dogetauthenticationinfo(authenticationtoken token) throws authenticationexception {
string accesstoken = (string) token.getprincipal();
// 根据accesstoken,查询用户信息
long userid = jwtutil.getuseridfromtoken(accesstoken);
assert.notnull(userid, "token已过期,请重新登入");
// 查询用户信息
sysuserentity userentity = userservice.getuser(userid);
// 转换成userdetail对象
userdetail userdetail = convertutils.sourcetotarget(userentity, userdetail.class);
simpleauthenticationinfo info = new simpleauthenticationinfo(userdetail, accesstoken, getname());
return info;
}
}4.6 token类
public class oauth2token implements authenticationtoken {
private string token;
public oauth2token(string token) {
this.token = token;
}
@override
public string getprincipal() {
return token;
}
@override
public object getcredentials() {
return token;
}
}4.7 shiro获取用户信息工具类
public class securityuser {
public static subject getsubject() {
try {
return securityutils.getsubject();
} catch (exception e) {
return null;
}
}
/**
* 获取用户信息
*/
public static userdetail getuser() {
subject subject = getsubject();
if (subject == null) {
return new userdetail();
}
userdetail user = (userdetail) subject.getprincipal();
if (user == null) {
return new userdetail();
}
return user;
}
public static string gettoken() {
return getuser().gettoken();
}
/**
* 获取用户id
*/
public static long getuserid() {
return getuser().getid();
}
}五、双token实现原理
5.1 token结构对比
| token类型 | 有效期 | 存储位置 | 包含信息 |
|---|---|---|---|
| access token | 15分钟 | 客户端 | 用户id、用户名称,类型,过期时间,角色(可选)、权限(可选) |
| refresh token | 7天 | 客户端,redis | 用户id、类型 |
5.2 核心流程图
六、完整代码示例
6.1 登录控制器(authcontroller.java)
@restcontroller
public class authcontroller {
@postmapping("/login")
public result login(@requestbody loginrequest request) {
user user = userservice.findbyusername(request.getusername());
if (user == null || !user.getpassword().equals(request.getpassword())) {
return result.error("账号或密码错误");
}
string accesstoken = jwtutil.createaccesstoken(
user.getusername(),
user.getid()
);
string refreshtoken = jwtutil.createrefreshtoken(user.getusername(),user.getid());
return result.success(map.of(
"accesstoken", accesstoken,
"refreshtoken", refreshtoken
));
}
@postmapping("/refreshtoken")
public result<string> refreshtoken(@requestheader("refreshtoken") string refreshtoken) {
long userid = jwtutil.getuseridfromrefreshtoken(refreshtoken);
//if(userid==null)userid=1904748826795986946l;
sysuserdto user = sysuserservice.getbyuserid(userid);
assert.notnull(user, "token异常,非法登入");
string newaccesstoken = jwtutil.createtoken(user.getid(),user.getusername());
jwtutil.refreshaccesstoken(refreshtoken);//自动续租
return new result<string>().ok(newaccesstoken);
}
}七、双token优势总结
| 维度 | 单token方案 | 双token方案 |
|---|---|---|
| 安全性 | 单一token泄露风险高 | access token短期有效,refresh token双存储(可自动延迟过期时间,并保证安全性) |
| 用户体验 | 频繁登录/重新认证 | 自动续期,用户无感知 |
| 合规性 | 不符合oauth2.0标准 | 完全遵循oauth2.0标准流程 |
八 、补充
为确保系统的安全性,本方案采用了单刷新token绑定机制。具体而言,每个用户的刷新token(refresh token)与单一设备或终端绑定。当同一用户在其他设备或终端上登录时,新的登录操作将导致之前设备的刷新token失效(通常伴随access token的到期登入失效)。这种机制有效防止了同一账户在多设备间的异常并行登录,增强了账户的安全性。
然而,根据不同的业务需求和安全策略,开发者可以根据实际情况对token管理机制进行调整。例如,若业务场景允许多设备同时在线,可以修改token绑定策略,支持多刷新token共存。此外,开发者们也欢迎在下方评论区提出自己的建议,共同探讨和优化。
到此这篇关于springboot集成shiro+jwt(hutool)完整代码示例的文章就介绍到这了,更多相关springboot shiro jwt集成内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论