Shiro之与SpringBoot整合
一、Shiro与SpringBoot整合
1.添加环境依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
2.自定义realm
自定义Realm实现只需继承AuthorizingRealm类,然后实现doGetAuthorizationInfo()和doGetAuthenticationInfo()方法。
authorization为授权,批准的意思,即获取用户的角色和权限等信息;
authentication认证,身份验证的意思,即登录时验证用户的合法性,比如验证用户名和密码。
public class CustomRealm extends AuthorizingRealm {
@Override
public void setName(String name) {
super.setName("customRealm");
}
@Autowired
private UserService userService;
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取认证的用户数据
User user = (User) SecurityUtils.getSubject().getPrincipal();
String userName = user.getUserName();
//构造认证数据
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 获取用户角色集
List<Role> roleList = userService.findByUserName(userName);
Set<String> roleSet = new HashSet<String>();
for (Role role : roleList) {
roleSet.add(role.getName());
}
// for (Role role : roleList) {
// simpleAuthorizationInfo.addRole(role.getName());
// }
simpleAuthorizationInfo.setRoles(roleSet);
// 获取用户权限集
List<Permission> permissionList = userPermissionMapper.findByUserName(userName);
Set<String> permissionSet = new HashSet<String>();
for (Permission p : permissionList) {
permissionSet.add(p.getName());
}
// for (Permission permission:permissionList) {
// simpleAuthorizationInfo.addStringPermission(permission.getName());
// }
simpleAuthorizationInfo.setStringPermissions(permissionSet);
return simpleAuthorizationInfo;
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1.获取登录的upToken
UsernamePasswordToken upToken = (UsernamePasswordToken)authenticationToken;
//2.获取输入的用户名密码
String username = oken.getUsername();
String password = new String(upToken.getPassword());
//3.数据库查询用户
User user = userService.findByName(username);
//4.用户存在并且密码匹配存储用户数据
// Shiro具有丰富的运行时AuthenticationException层次结构,可以准确指出认证失败的原因
if (user == null) {
throw new UnknownAccountException("用户名或密码错误!");
}
if (!password.equals(user.getPassword())) {
throw new IncorrectCredentialsException("用户名或密码错误!");
}
if (user.getStatus().equals("0")) {
throw new LockedAccountException("账号已被锁定,请联系管理员!");
}
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), this.getName());
return simpleAuthenticationInfo;
}
}
3.编写登录方法
@RequestMapping(value="/login")
public String login(String username,String password) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
try {
subject.login(token);
return "登录成功!";
} catch (UnknownAccountException e) {
return e.getMessage();
} catch (IncorrectCredentialsException e) {
return e.getMessage();
} catch (LockedAccountException e) {
return e.getMessage();
} catch (AuthenticationException e) {
return "认证失败!";
}
}
4.Shiro配置
@Configuration
public class ShiroConfiguration {
//配置自定义的Realm
@Bean
public CustomRealm getRealm() {
return new CustomRealm();
}
//配置安全管理器
@Bean
public SecurityManager securityManager(CustomRealm realm) {
//将自定义的realm交给安全管理器统一调度管理,使用默认的安全管理器
//1.构造方式
// DefaultWebSecurityManager securityManager = newDefaultWebSecurityManager(realm);
//2.属性设值方式
DefaultWebSecurityManager securityManager = newDefaultWebSecurityManager();
securityManager.setRealm(realm);
return securityManager;
}
//配置Shiro顾虑器Filter工厂,设置对应的过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
//1.创建shiro过滤器工厂
ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
//2.设置安全管理器
filterFactory.setSecurityManager(securityManager);
//3.配置登录页面,登录成功页面,验证未成功页面
filterFactory.setLoginUrl("/index"); //设置登录页面
// 设置登录成功后跳转的url
shiroFilterFactoryBean.setSuccessUrl("/home");
//授权失败跳转页面
filterFactory.setUnauthorizedUrl("/index");
// 配置退出过滤器,具体退出代码Shiro已经实现
filterChainDefinitionMap.put("/logout", "logout");
//4.添加shiro内置过滤器,实现权限相关的url拦截
/**
* 常见过滤器:
* anon:无需认证(登录)可以访问
* authc:必须认证才可以访问
* user:如果使用Remember Me的功能,可以直接访问
* perms:该资源必须得到资源权限才可以访问
* role:该资源必须得到角色权限才可以访问
* filterChain基于短路机制,即最先匹配原则 /user/**=anon /user/addUser=authc 永远不会执行
*/
Map<String,String> filterMap = new LinkedHashMap<String,String>();
//静态资源不拦截
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
// anno :匿名访问,表明此链接所有人可以访问
filterMap.put("/index", "anon");
//authc :认证后访问(表明此链接需登录认证成功之后可以访问)
//参数可写多个,表示是拥有某个或某些角色才能通过,不具备则跳转到setUnauthorizedUrl设置地址
filterMap.put("/home/details", "roles[admin]");
//参数可写多个,表示需要拥有某个或某些权限才能通过,不具备则跳转到setUnauthorizedUrl设置地址
filterMap.put("/home/details", "perms[getDetails]");
//authc :认证后访问,表明此链接需登录认证成功之后可以访问
filterMap.put("/home/**", "authc");
//5.设置过滤器
filterFactory.setFilterChainDefinitionMap(filterMap);
return filterFactory;
}
//配置shiro注解支持
@Bean
public AuthorizationAttributeSourceAdvisorauthorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new
AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
5.权限相关注解
使用权限注解需配置开启注解支持
// 表示当前Subject已经通过login进行了身份验证;即Subject.isAuthenticated()返回true。
@RequiresAuthentication
// 表示当前Subject已经身份验证或者通过记住我登录的。
@RequiresUser
// 表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。
@RequiresGuest
// 表示当前Subject需要角色admin和user。
@RequiresRoles(value = {"admin", "user"}, logical = Logical.AND)
// 表示当前Subject需要权限user:add或user:update。
@RequiresPermissions(value = {"user:add", "user:update"}, logical = Logical.OR)
6.Shiro授权
1.基于配置授权
//4.配置过滤器集合
Map<String,String> filterMap = new LinkedHashMap<String,String>();
// anno :匿名访问,表明此链接所有人可以访问
filterMap.put("/index", "anon");
//authc :认证后访问(表明此链接需登录认证成功之后可以访问)
//参数可写多个,表示是拥有某个或某些角色才能通过,不具备则跳转到setUnauthorizedUrl设置地址
filterMap.put("/home/details", "roles[admin]");
//参数可写多个,表示需要拥有某个或某些权限才能通过,不具备则跳转到setUnauthorizedUrl设置地址
filterMap.put("/home/details", "perms[getDetails]");
//authc :认证后访问,表明此链接需登录认证成功之后可以访问
filterMap.put("/**", "authc");
//5.设置过滤器
filterFactory.setFilterChainDefinitionMap(filterMap);
2.基于注解授权
基于角色授权:
@RequiresRoles(value = "admin")
public String select() {
return "查询成功";
}
基于权限授权:
@RequiresPermissions(value = "getDetails")
public String select() {
return "查询成功";
}
7.自定义权限异常处理器
1.基于配置方式进行授权,若用户不具备权限,目标地址不会执行。会跳转到filterFactory.setUnauthorizedUrl指定的url地址。
2.基于注解配置方式进行授权,若用户不具备操作权限,目标方法不会被执行,并且会抛出AuthorizationException 异常。
/**
* 自定义权限异常处理器
*/
@ControllerAdvice
public class AuthorizationExceptionHandler {
@ExceptionHandler(value = AuthorizationException.class)
@ResponseBody
public String error(HttpServletRequest request, HttpServletResponse response,AuthorizationException e) {
return "您没有相关权限!";
}
}
二、Shiro之编码加密
1.添加密码凭证匹配器
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 使用md5 算法进行加密
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 设置散列次数: 意为加密几次
hashedCredentialsMatcher.setHashIterations(1024);
return hashedCredentialsMatcher;
}
2.注册给Realm
@Bean
public CustomRealm getRealm() {
CustomRealm realm = new CustomRealm();
realm.setCredentialsMatcher(hashedCredentialsMatcher());
return realm;
}
3.注册时对密码加密
/**
* 密码加密
* @param password 明文密码
* @param salt 盐
* @return
*/
public static String md5Hash(String password, String salt) {
return new Md5Hash(password, salt, 1024).toString();
}
4.自定义Realm认证方法作以下处理
return new SimpleAuthenticationInfo(用户,用户密码,盐,当前Realm的类名);
return new SimpleAuthenticationInfo(user,user.getPassword(), ByteSource.Util.bytes(user.getSalt()),getName());
5.登录调用UsernamePasswordToken
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
Subject subject = SecurityUtils.getSubject();
subject.login(usernamePasswordToken);
三、Shiro之记住我
当成功登录后,关闭浏览器然后再打开搜保护资源时,页面会跳转到登录页,之前的登录因为浏览器的关闭已经失效了,Shiro为提供了Remember Me的功能,用户的登录状态不会因为浏览器的关闭而失效,直到Cookie过期。
1.修改Shiro配置
基于上述配置文件添加cookie对象与cookie管理对象
/**
* cookie对象
* @return
*/
@Bean
public SimpleCookie rememberMeCookie(){
//参数remember是cookie的名称,对应前端的checkbox的name = remember
SimpleCookie simpleCookie = new SimpleCookie("remember");
// 设置cookie的过期时间,单位为秒
simpleCookie.setMaxAge(60*60*60*24*7);
return simpleCookie;
}
/**
* cookie管理对象
* @return
*/
@Bean
public CookieRememberMeManager rememberMeManager(){
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
// rememberMe cookie加密的密钥
cookieRememberMeManager.setCipherKey(Base64.decode("abcdefg"));
return cookieRememberMeManager;
}
将cookie管理对象设置到SecurityManager中
@Bean
public SecurityManager securityManager(CustomRealm realm) {
//将自定义的realm交给安全管理器统一调度管理,使用默认的安全管理器
//1.构造方式
// DefaultWebSecurityManager securityManager = newDefaultWebSecurityManager(realm);
//2.属性设值方式
DefaultWebSecurityManager securityManager = newDefaultWebSecurityManager();
securityManager.setRealm(realm);
//设置记住我
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
修改权限配置
将ShiroFilterFactoryBean的filterMap.put("/**", "authc");更改为filterMap.put("/**", "user")
user指的是用户认证通过或者配置了Remember Me记住用户登录状态后可访问。
2.更改LoginController
@RequestMapping(value="/login")
public String login(String username,String password, Boolean rememberMe) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username,password,rememberMe);
try {
subject.login(token);
return "登录成功!";
} catch (UnknownAccountException e) {
return e.getMessage();
} catch (IncorrectCredentialsException e) {
return e.getMessage();
} catch (LockedAccountException e) {
return e.getMessage();
} catch (AuthenticationException e) {
return "认证失败!";
}
}
四、Shiro之使用缓存
权限在短时间内基本不会变化,加入缓存可以使权限相关操作尽可能快,避免频繁访问操作数据库
Shiro提供了Cache的抽象,并没有直接提供相应的实现,需要去进行缓存实现,常用Redis和Ehcache缓存实现
1.Redis实现缓存
引入Redis依赖
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>x.x.x</version>
</dependency>
配置Redis
spring:
redis:
host: localhost
port: 6379
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
timeout: 0
修改Shiro配置
在Shiro配置文件添加RedisManager与RedisCacheManager配置
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
return redisManager;
}
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
在SecurityManager中加入RedisCacheManager
@Bean
public SecurityManager securityManager(CustomRealm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setRememberMeManager(rememberMeManager());
//设置记住我
securityManager.setCacheManager(cacheManager());
return securityManager;
}
2.Ehcache实现缓存
添加Ehcache依赖
<!-- shiro ehcache -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>x.x.x</version>
</dependency>
<!-- ehchache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>x.x.x</version>
</dependency>
Ehcache配置
在src/main/resource/config路径下新增shiro-ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="java.io.tmpdir/Tmp_EhCache" />
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120" />
<!-- 登录记录缓存锁定1小时 -->
<cache name="passwordRetryCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true" />
</ehcache>
Shiro配置
在Shiro配置中添加Ehcache缓存配置
@Bean
public EhCacheManager getEhCacheManager() {
EhCacheManager em = new EhCacheManager();
em.setCacheManagerConfigFile("classpath:config/shiro-ehcache.xml");
return em;
}
将缓存对象注入到SecurityManager中
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
securityManager.setRememberMeManager(rememberMeManager());
securityManager.setCacheManager(getEhCacheManager());
return securityManager;
}
五、添加Shiro会话管理
在shiro里所有的用户的会话信息都会由Shiro来进行控制,shiro提供的会话可以用于JavaSE/JavaEE环境,不依赖于任何底层容器,可以独立使用,是完整的会话模块。通过Shiro的会话管理器(SessionManager)进行统一的会话管理。
shiro提供了三个默认实现:
1. DefaultSessionManager:用于JavaSE环境
2. ServletContainerSessionManager:用于Web环境,直接使用servlet容器的会话。(默认会话管理器)
3. DefaultWebSessionManager:用于web环境,自己维护会话(自己维护着会话,直接废弃了Servlet容器的会话管理)。
在web程序中,通过shiro的Subject.login()方法登录成功后,用户的认证信息实际上是保存在HttpSession中
如果使用默认会话管理,用户信息只会保存到一台服务器上,在分布式系统或者微服务架构下,都是通过统一的认证中心进行用户认证,此时就需要使用DefaultWebSessionManager,由自己维护会话。
1.添加依赖
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.2.3</version>
</dependency>
2.配置redis
redis:
host: 127.0.0.1
port: 6379
#自定义redis key
redis:
host: 127.0.0.1
password:
port: 6379
dataBase: 6
3.使用默认会话管理器
使用DefaultWeb Session Manager自己维护会话,但是需要配置Session dao的实现
// 缓存配置
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
return redisManager;
}
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
//配置SessionDao,使用shiro-redis基于redis的sessionDao的实现
@Bean
public RedisSessionDAO sessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
//会话管理器
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(sessionDAO());
return sessionManager;
}
注册session管理器与自定义缓存管理器
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = newDefaultWebSecurityManager();
// 自定义session管理注册到安全管理器中
securityManager.setSessionManager(sessionManager());
// 自定义缓存实现注册到安全管理器中
securityManager.setCacheManager(cacheManager());
return securityManager;
}
4.自定义shiro会话管理器
自定义Session dao维护会话操作,在 Session Dao指定Reds配置
/**
* 自定义的sessionManager
*/
public class CustomSessionManager extends DefaultWebSessionManager {
/**
* 头信息中有sessionid
* 请求头:Authorization: sessionid
* sessionid从请求头中获取
* <p>
* 指定sessionId的获取方式
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse
response) {
//获取请求头Authorization中的数据
String id = WebUtils.toHttp(request).getHeader("Authorization");
if (StringUtils.isEmpty(id)) {
//如果没有携带,生成新的sessionId
return super.getSessionId(request, response);
} else {
//返回sessionId;
//从请求头获取SessionId
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header");
//sessionId的值是什么
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
//sessionId需不需要验证
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
}
}
}
修改Shiro配置,配置Shiro基于redis的会话管理
@Value("${redis.host}")
private String host;
@Value("${redis.port}")
private int port;
@Value("${redis.dataBase}")
private int dataBase;
@Value("${redis.password}")
private String password;
//配置shiro的RedisManager,通过shiro-redis包提供的RedisManager统一对redis操作
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host + ":" + port);
if (StringUtils.isNotBlank(password)) {
redisManager.setPassword(password);
}
redisManager.setDatabase(dataBase);
return redisManager;
}
//配置Shiro的缓存管理器,替换Shiro内部自己的本地缓存机制,全部替换redis实现
@Bean
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
//配置SessionDao,使用shiro-redis基于redis的sessionDao的实现
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
//配置会话管理器,指定sessionDao的依赖关系
@Bean
public DefaultWebSessionManager sessionManager() {
CustomSessionManager sessionManager = new CustomSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
注册自定义session管理器与自定义缓存管理器
//配置安全管理器
@Bean
public SecurityManager securityManager(CustomRealm realm) {
DefaultWebSecurityManager securityManager = newDefaultWebSecurityManager();
// 自定义session管理注册到安全管理器中
securityManager.setSessionManager(sessionManager());
// 自定义缓存实现注册到安全管理器中
securityManager.setCacheManager(cacheManager());
securityManager.setRealm(realm);
return securityManager;
}
根据Authorization获取当前登录用户的信息
@Autowired
private RedisSessionDAO redisSessionDAO;
@RequestMapping("/getUser")
public User getUserByHeader(ServletRequest request) throws Exception{
//前端请求的headers中必须传入Authorization头信息
String id = WebUtils.toHttp(request).getHeader("Authorization");
Session session = redisSessionDAO.readSession(id);
Object obj = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
SimplePrincipalCollection simplePrincipalCollection = (SimplePrincipalCollection) obj;
String userStr = JSON.toJSON(simplePrincipalCollection.getPrimaryPrincipal()).toString();
User user = JSON.parseObject(userStr, User.class);
return user;
}
5.在线人数统计与强制用户下线
sessionManager管理器
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
Collection<SessionListener> listeners = new ArrayList<SessionListener>();
listeners.add(new ShiroSessionListener());
sessionManager.setSessionListeners(listeners);
sessionManager.setSessionDAO(sessionDAO());
return sessionManager;
}
ShiroSessionListener会话监听器
ShiroSessionListener为org.apache.shiro.session.SessionListener接口的实现,维护着一个原子类型的Integer对象,用于统计在线Session的数量
public class ShiroSessionListener implements SessionListener{
private final AtomicInteger sessionCount = new AtomicInteger(0);
@Override
public void onStart(Session session) {
sessionCount.incrementAndGet();
}
@Override
public void onStop(Session session) {
sessionCount.decrementAndGet();
}
@Override
public void onExpiration(Session session) {
sessionCount.decrementAndGet();
}
}
在线用户UserOnline
public class UserOnline implements Serializable{
/**
* session id
*/
private String id;
/**
* 用户id
*/
private String userId;
/**
* 用户名称
*/
private String username;
/**
* 用户主机地址
*/
private String host;
/**
* 登录IP
*/
private String systemHost;
/**
* 状态
*/
private String status;
/**
* session创建时间
*/
private Date startTimestamp;
/**
* session最后访问时间
*/
private Date lastAccessTime;
/**
* 超时时间
*/
private Long timeout;
}
public interface SessionService {
/**
* 在线用户
* @return
*/
List<UserOnline> list();
/**
* 强制用户下线
* @param sessionId
* @return
*/
boolean compulsoryLogout(String sessionId);
}
@Service("sessionService")
public class SessionServiceImpl implements SessionService {
@Autowired
private RedisSessionDAO redisSessionDAO;
@Override
public List<UserOnline> list() {
List<UserOnline> list = new ArrayList<>();
UserOnline userOnline ;
//获取所有有效的Session
Collection<Session> sessions = redisSessionDAO.getActiveSessions();
for (Session session : sessions) {
userOnline=new UserOnline();
User user = new User();
SimplePrincipalCollection principalCollection = new SimplePrincipalCollection();
if (session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) == null) {
continue;
} else {
// 通过session获取用户信息。
principalCollection = (SimplePrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
user = (User) principalCollection.getPrimaryPrincipal();
userOnline.setUsername(user.getUserName());
userOnline.setUserId(user.getId().toString());
}
userOnline.setId((String) session.getId());
userOnline.setHost(session.getHost());
userOnline.setStartTimestamp(session.getStartTimestamp());
userOnline.setLastAccessTime(session.getLastAccessTime());
Long timeout = session.getTimeout();
// 用户被踢出后Session Time置为0
if (timeout == 0L) {
userOnline.setStatus("离线");
} else {
userOnline.setStatus("在线");
}
userOnline.setTimeout(timeout);
list.add(userOnline);
}
return list;
}
@Override
public boolean compulsoryLogout(String sessionId) {
Session session = redisSessionDAO.readSession(sessionId);
session.setTimeout(0);
return true;
}
}
六、Shiro之Shiro标签
以在Thymeleaf使用Shiro标签为例
首先Thymeleaf官方并没有提供Shiro的标签,需要引入第三方实现(https://github.com/theborakompanioni/thymeleaf-extras-shiro
)
1.引入依赖
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
2.修改Shiro配置
在ShiroConfig中配置该方言标签
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
3.HTML页面
在html页面中使用Shiro标签需要给html标签添加xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"
<!DOCTYPE html>
<html
xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<p>你好![[${user.userName}]],欢迎登陆</p>
<p shiro:hasRole="user">用户管理角色</p>
<p shiro:hasRole="test">测试人员角色</p>
<div>
<a shiro:hasPermission="user:add" th:href="@{/user/add}">新增用户</a>
<a shiro:hasPermission="user:delete" th:href="@{/user/delete}">删除用户</a>
</div>
</body>
</html>
七、Shiro之JWT
Shiro是基于session保持会话的,是有状态的。
JWT则是无状态的,服务端不保存session,而是生成token发送给客户端进行保存,之后的所有的请求都需要携带token,再对token进行解析判断
添加依赖
<!-- <dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.5.3</version>
</dependency>
<!-- jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.1</version>
</dependency>
定义JWTUtil
public class JWTUtil {
/**
* 指定一个token过期时间(毫秒)
*/
private static final long EXPIRE_TIME = 24 * 60 * 60 * 1000;
/**
* 生成 token
*
* @param username
* @param secret
* @return token
*/
public static String sign(String username, String secret) {
try {
//统一字符串转小写
username = StringUtils.lowerCase(username);
// 过期时间
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
// 传入在验证或签名实例中使用的密匙,得到HMAC256算法
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息的token
return JWT.create()
.withClaim("username", username)
//过期时间
.withExpiresAt(date)
//签名算法
.sign(algorithm);
} catch (Exception e) {
return null;
}
}
/**
* 校验token是否正确
*
* @param token
* @param secret
* @return
*/
public static boolean verify(String token, String secret) {
try {
// 传入在验证或签名实例中使用的密匙,得到HMAC256算法
Algorithm algorithm = Algorithm.HMAC256(secret);
// 提供算法得到带有用于验证令牌签名的算法的JWTVerifier构建器
JWTVerifier verifier = JWT.require(algorithm).build();
//效验TOKEN
verifier.verify(token);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 在token中获取username信息
*
* @return token中包含的用户名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 判断是否过期
*/
public static boolean isExpire(String token) {
DecodedJWT jwt = JWT.decode(token);
return jwt.getExpiresAt().getTime() < System.currentTimeMillis();
}
}
定义JWTToken
将Jwt与Shiro对接,定义JWTToken,将令牌封装成认证对象,shiro会拦截所有请求,然后验证请求提交的Token是否合法
/**
* 自定义shiro接口token,通过这个类将string的token转型成AuthenticationToken,供shiro使用
*
* 需要重写getPrincipal和getCredentials方法
*/
public class JWTToken implements AuthenticationToken {
private String token;
private String exipreAt;
public JWTToken(String token) {
this.token = token;
}
public JWTToken(String token, String exipreAt) {
this.token = token;
this.exipreAt = exipreAt;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getExipreAt() {
return exipreAt;
}
public void setExipreAt(String exipreAt) {
this.exipreAt = exipreAt;
}
}
自定义Realm
public class ShiroRealm extends AuthorizingRealm {
/**
* 限定这个realm只能处理JwtToken
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
/**
* `
* 授权模块,获取用户角色和权限
*
* @param token token
* @return AuthorizationInfo 权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection token) {
String username = JWTUtil.getUsername(token.toString());
// TODO 操作数据库获取角色权限信息
List<User> users = new ArrayList<>();
User user1 = new User("admin", "a18d2133f593d7b0e3ed488560404083", new HashSet<>(Collections.singletonList("admin")), new HashSet<>(Arrays.asList("user:add", "user:delete")));
User user2 = new User("test", "a18d2133f593d7b0e3ed488560404083", new HashSet<>(Collections.singletonList("test")), new HashSet<>(Arrays.asList("user:delete")));
users.add(user1);
users.add(user2);
User user = users.stream().filter(u -> StringUtils.equalsIgnoreCase(username, u.getUsername())).findFirst().orElse(null);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 获取用户角色集
simpleAuthorizationInfo.setRoles(user.getRole());
// 获取用户权限集
simpleAuthorizationInfo.setStringPermissions(user.getPermission());
return simpleAuthorizationInfo;
}
/**
* 用户认证
*
* @param authenticationToken 身份认证 token
* @return AuthenticationInfo 身份认证信息
* @throws AuthenticationException 认证相关异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// token是从JWTFilter的executeLogin方法传递过来的,已经经过解密,是JwtToken中重写这个方法
String token = (String) authenticationToken.getCredentials();
// 获得username
String username = JWTUtil.getUsername(token);
if (StringUtils.isBlank(username)) {
throw new AuthenticationException("token校验不通过");
}
//TODO 数据操作查询用户信息
List<User> users = new ArrayList<>();
// 模拟两个用户:
// 1. 用户名 admin,密码 123456,角色 admin(管理员),权限 "user:add","user:view"
// 1. 用户名 scott,密码 123456,角色 regist(注册用户),权限 "user:view"
users.add(new User(
"admin",
"a18d2133f593d7b0e3ed488560404083",
new HashSet<>(Collections.singletonList("admin")),
new HashSet<>(Arrays.asList("user:add", "user:view"))));
users.add(new User(
"test",
"a18d2133f593d7b0e3ed488560404083",
new HashSet<>(Collections.singletonList("regist")),
new HashSet<>(Collections.singletonList("user:view"))));
User user = users.stream().filter(u -> StringUtils.equalsIgnoreCase(username, u.getUsername())).findFirst().orElse(null);
if (user == null) {
throw new AuthenticationException("用户名或密码错误");
}
if (!JWTUtil.verify(token, username, user.getPassword())) {
throw new AuthenticationException("token校验不通过");
}
//toke过期
if (JWTUtil.isExpire(token)) {
throw new ExpiredCredentialsException("toke过期");
}
// return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), ByteSource.Util.bytes("salt"), "shiro_realm");
return new SimpleAuthenticationInfo(token, token, "shiro_realm");
}
}
定义JWTFilter
/**
* jwt过滤器,作为shiro的过滤器,对请求进行拦截并处理
* <p>
* 跨域配置
*/
public class JWTFilter extends BasicHttpAuthenticationFilter {
private static final String TOKEN = "Token";
private AntPathMatcher pathMatcher = new AntPathMatcher();
/**
* 过滤器拦截请求的入口方法
*
* @param request
* @param response
* @param mappedValue
* @return
* @throws UnauthorizedException
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
// Shiro配置中添加过滤
// HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// String url="/user/register,/user/login";
// String[] anonUrl = StringUtils.splitByWholeSeparatorPreserveAllTokens(url, ",");
//
// boolean match = false;
// for (String u : anonUrl) {
// if (pathMatcher.match(u, httpServletRequest.getRequestURI())) {
// match = true;
// }
// }
// if (match) {
// return true;
// }
if (isLoginAttempt(request, response)) {
try {
//token验证
return executeLogin(request, response);
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
/**
* 如果传入请求是基于登录的尝试,则为 true,否则为 false
*
* @param request
* @param response
* @return
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
System.out.println("尝试登录");
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader(TOKEN);
return token != null;
}
/**
* 进行token的验证
*
* @param request
* @param response
* @return
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws IOException {
//在请求头中获取token
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
//前端命名Token
String token = httpServletRequest.getHeader(TOKEN);
//token不存在
if (token == null || "".equals(token)) {
this.responseResult(response, "0", "无token,无权访问,请先登录");
return false;
}
//token存在,进行验证
JWTToken jwtToken = new JWTToken(token);
try {
//通过subject,提交给Realm进行登录验证
//SecurityUtils.getSubject().login(jwtToken);
getSubject(request, response).login(jwtToken);
return true;
} catch (ExpiredCredentialsException e) {
this.responseResult(response, "0", "token过期,请重新登录");
return false;
} catch (ShiroException e) {
this.responseResult(response, "0", "token被伪造,无效token");
return false;
}
}
/**
* isAccessAllowed()方法返回false,进入onAccessDenied方法
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
System.out.println("验证不通过,进入onAccessDenied()");
return false;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域会先发送一个option请求,让option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* json形式返回结果token验证失败信息
*/
private void responseResult(ServletResponse response, String code, String msg) throws IOException {
HashMap<String, Object> map = new HashMap<>();
map.put("msg", msg);
map.put("code", code);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
ObjectMapper mapper = new ObjectMapper();
String jsonRes = mapper.writeValueAsString(map);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json; charset=utf-8");
httpServletResponse.getOutputStream().write(jsonRes.getBytes());
}
}
Shiro Config
@Configuration
public class ShiroConfig {
/**
* 创建ShiroFilter(用于拦截所有请求,对受限资源进行Shiro的认证和授权判断)
*
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置 securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 在Shiro过滤器链上加入JWTFilter,添加自己的过滤器并且取名为jwt
LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
filters.put("jwt", new JWTFilter());
shiroFilterFactoryBean.setFilters(filters);
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//登录路径、注册路径都需要放行不进行拦截
filterChainDefinitionMap.put("/user/login", "anon");
filterChainDefinitionMap.put("/user/register", "anon");
// 所有请求都要经过 jwt过滤器
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//给安全管理器设置realm
securityManager.setRealm(shiroRealm());
//关闭shiro的session(无状态的方式使用shiro)
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
/**
* 加密
* 使用了UsernamePasswordToken并且有对password进行加密的才需要
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 使用md5 算法进行加密
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 设置散列次数: 意为加密几次
hashedCredentialsMatcher.setHashIterations(1024);
return hashedCredentialsMatcher;
}
/**
* 配置 Realm
*
* @return
*/
@Bean
public ShiroRealm shiroRealm() {
ShiroRealm realm = new ShiroRealm();
//使用了UsernamePasswordToken并且有对password进行加密的才需要
//realm.setCredentialsMatcher(hashedCredentialsMatcher());
return realm;
}
/**
* 开启对 Shiro 注解的支持
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
/**
* 强制使用cglib,防止重复代理和可能引起代理出错的问题
*
* 使用shiro-spring-boot-starter遇到使用shiro注解导致controller类中spring注解失效,需使用以下配置
*
* 使用shiro-spring则不需要使用以下配置
* @return
*/
// @Bean
// @DependsOn({"lifecycleBeanPostProcessor"})
// public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
// DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// advisorAutoProxyCreator.setProxyTargetClass(true);
// return advisorAutoProxyCreator;
// }
}
执行测试
@RestController
@RequestMapping("/user")
public class LoginController {
@PostMapping("/login")
public Object login(String username, String password) throws Exception {
// UsernamePasswordToken认证方式
// Subject subject = SecurityUtils.getSubject();
// UsernamePasswordToken token = new UsernamePasswordToken(username,password);
// subject.login(token);
// JWT认证方式
//TODO 用户验证是否存在
username = StringUtils.lowerCase(username);
//盐 + 密码 + 1024次散列,作为token生成的密钥
Md5Hash md5Hash = new Md5Hash(password, "salt", 1024);
//toHex转换成16进制,32为字符
password = md5Hash.toHex();
// 用户验证成功生成 Token
String token = JWTUtil.sign(username, password);
//转换成jwtToken(才可以被shiro识别)
JWTToken jwtToken = new JWTToken(token);
//拿到Subject对象
Subject subject = SecurityUtils.getSubject();
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("token", null);
userInfo.put("user", "user");
//正式进行认证验证
try {
subject.login(jwtToken);
userInfo.put("token", token);
userInfo.put("msg", "登录成功");
} catch (UnknownAccountException e) {
userInfo.put("msg", "无效用户,用户不存在");
e.printStackTrace();
} catch (IncorrectCredentialsException e) {
userInfo.put("msg", "密码输入错误");
e.printStackTrace();
} catch (ExpiredCredentialsException e) {
userInfo.put("msg", "token过期,请重新登录");
e.printStackTrace();
}
return userInfo;
}
/**
* 需要登录才能访问
*/
@GetMapping("/test1")
public String test1() {
return "success";
}
/**
* 需要 admin 角色才能访问
*/
@GetMapping("/test2")
@RequiresRoles("admin")
public String test2() {
return "success";
}
/**
* 需要 "user:add" 权限才能访问
*/
@GetMapping("/test3")
@RequiresPermissions("user:add")
public String test3() {
return "success";
}
}
{
"msg": "登录成功",
"user": "user",
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MjkzNTI3MzUsInVzZXJuYW1lIjoidGVzdCJ9.nBrTh6SbJocT14R-nh4VhQlaAyqh2J-ebTuk1Mj9AVY"
}
令牌自动刷新
定义ThreadLocalToken存储令牌
@Component
public class ThreadLocalToken {
private ThreadLocal<String> local = new ThreadLocal<>();
public void setToken(String token) {
local.set(token);
}
public String getToken() {
return local.get();
}
public void clear() {
local.remove();
}
}
定义JWTFilter 继承AuthenticatingFilter重写部分方法
@Component
// 使用threadLocalToken,所以该类应该是多例
@Scope("prototype")
public class JWTFilter extends AuthenticatingFilter {
/**
* 每个请求线程都有自己独立的threadLocalToken
*/
@Autowired
private ThreadLocalToken threadLocalToken;
@Value("${zd.jwt.cache-expire}")
private int cacheExpire;
@Value("${zd.jwt.secret}")
private String secret;
@Autowired
private RedisTemplate redisTemplate;
/**
* 拦截请求之后,用于把令牌字符串封装成令牌对象
*
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
// 获取请求token
String token = getRequestToken(req);
if (StrUtil.isBlank(token)) {
return null;
}
return new JWTToken(token);
}
/**
* 拦截请求,判断请求是否需要被shiro处理
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest req = (HttpServletRequest) request;
// 放行Options请求
if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
return true;
}
return false;
}
/**
* 处理所有应该被shiro处理的请求
*
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
threadLocalToken.clear();
String token = getRequestToken(req);
if (StrUtil.isBlank(token)) {
resp.setStatus(HttpStatus.UNAUTHORIZED.value());
resp.getWriter().print("无效的令牌");
return false;
}
try {
JWTUtil.verify(token, secret);
} catch (TokenExpiredException e) {
if (redisTemplate.hasKey(token)) {
redisTemplate.delete(token);
String userName = JWTUtil.getUsername(token);
token = JWTUtil.sign(userName, secret);
redisTemplate.opsForValue().set(token, userName, cacheExpire, TimeUnit.DAYS);
threadLocalToken.setToken(token);
} else {
resp.setStatus(HttpStatus.UNAUTHORIZED.value());
resp.getWriter().print("令牌已过期");
return false;
}
} catch (Exception e) {
resp.setStatus(HttpStatus.UNAUTHORIZED.value());
resp.getWriter().print("无效的令牌");
return false;
}
boolean bool = executeLogin(request, response);
return bool;
}
/**
* 登录失败
* @param token
* @param e
* @param request
* @param response
* @return
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
resp.setStatus(HttpStatus.UNAUTHORIZED.value());
try {
resp.getWriter().print(e.getMessage());
} catch (Exception exception) {
}
return false;
}
@Override
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
super.doFilterInternal(request, response, chain);
}
/**
* 获取请求Token
* @param request
* @return
*/
private String getRequestToken(HttpServletRequest request) {
String token = request.getHeader("token");
if (StrUtil.isBlank(token)) {
token = request.getParameter("token");
}
return token;
}
}
利用AOP向客户端返回令牌信息
@Aspect
@Component
public class TokenAspect {
@Autowired
private ThreadLocalToken threadLocalToken;
@Pointcut("execution(public * cn.ybzy.demo.controller.*.*(..))")
public void aspect() {
}
@Around("aspect()")
public Object around(ProceedingJoinPoint point) throws Throwable {
HashMap<String, Object> map = (HashMap<String, Object>) point.proceed();
String token = threadLocalToken.getToken();
if (token != null) {
map.put("token", token);
threadLocalToken.clear();
}
return map;
}
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/137150.html