一、spring boot shiro 无状态token认证项目结构图
二、无状态spring boot shiro相关配置
2.1shiro redis 缓存配置
首先是实现shiro的cache接口
$title(RedisCache.java)
package net.xqlee.project.demo.shiro.config.shiro.cache;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
/**
* Redis的Shiro缓存对象实现
*
* @author xq
*
* @param <K>
* @param <V>
*/
public class RedisCache<K, V> implements Cache<K, V> {
private static final Logger logger = LoggerFactory.getLogger(RedisCache.class);
private RedisTemplate<K, V> redisTemplate;
private final static String PREFIX = "shiro-cache:";
private String cacheKey;
private long globExpire = 30;
@SuppressWarnings({ "rawtypes", "unchecked" })
public RedisCache(final String name, final RedisTemplate redisTemplate) {
this.cacheKey = PREFIX + name + ":";
this.redisTemplate = redisTemplate;
}
@Override
public V get(K key) throws CacheException {
logger.debug("Shiro从缓存中获取数据KEY值["+key+"]");
redisTemplate.boundValueOps(getCacheKey(key)).expire(globExpire, TimeUnit.MINUTES);
return redisTemplate.boundValueOps(getCacheKey(key)).get();
}
@Override
public V put(K key, V value) throws CacheException {
V old = get(key);
redisTemplate.boundValueOps(getCacheKey(key)).set(value);
return old;
}
@Override
public V remove(K key) throws CacheException {
V old = get(key);
redisTemplate.delete(getCacheKey(key));
return old;
}
@Override
public void clear() throws CacheException {
redisTemplate.delete(keys());
}
@Override
public int size() {
return keys().size();
}
@Override
public Set<K> keys() {
return redisTemplate.keys(getCacheKey("*"));
}
@Override
public Collection<V> values() {
Set<K> set = keys();
List<V> list = new ArrayList<>();
for (K s : set) {
list.add(get(s));
}
return list;
}
@SuppressWarnings("unchecked")
private K getCacheKey(Object k) {
return (K) (this.cacheKey + k);
}
}
接下来是实现shiro的CacheManager
$title(RedisCacheManager.java)
package net.xqlee.project.demo.shiro.config.shiro.cache;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
/**
* Redis的Shiro缓存管理器实现
*
* @author xq
*
*/
public class RedisCacheManager implements CacheManager {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
return new RedisCache<>(name, redisTemplate);
}
}
shiro的缓存配置就是以上两个文件了。至于redis的配置这里就不讲述了。
2.2自定义一个AuthenticationToken实现
自定义一个AuthenticationToken实现用于登录认证等。
$title(StatelessAuthenticationToken.java)
package net.xqlee.project.demo.shiro.config.shiro;
import org.apache.shiro.authc.AuthenticationToken;
import java.util.Map;
/**
* 自定义实现Shiro的一个认证Token
*
* @author xq
*/
public class StatelessAuthenticationToken implements AuthenticationToken {
/**
*
*/
private static final long serialVersionUID = 1L;
private String token;// 用户登录后获取的令牌;
// 构造函数
public StatelessAuthenticationToken() {
}
public StatelessAuthenticationToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return this;
}
}
这个类其实很简单。就是实现接口并添加了一个token的字段。用于后面进行认证。
2.3 自定义shiro的Realm
$title(UserRealm.java)
package net.xqlee.project.demo.shiro.config.shiro;
import net.xqlee.project.demo.shiro.pojo.LoginAccount;
import net.xqlee.project.demo.shiro.service.UserService;
import org.apache.shiro.authc.*;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* 实现一个基于JDBC的Realm,继承AuthorizingRealm可以看见需要重写两个方法,doGetAuthorizationInfo和doGetAuthenticationInfo
*
* @author xqlee
*/
@Component
public class UserRealm extends AuthorizingRealm {
private static final Logger logger = LoggerFactory.getLogger(UserRealm.class);
/*** 用户业务处理类,用来查询数据库中用户相关信息 ***/
@Autowired
UserService userService;
/**
* 该Realm仅支持自定义的StatelessAuthenticationToken类型Token,其他类型处理将会抛出异常
*
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof StatelessAuthenticationToken;
}
/***
* 获取用户授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
logger.info("##################执行Shiro权限认证##################");
// 获取用户名
LoginAccount account = (LoginAccount) principalCollection.getPrimaryPrincipal();
// 判断用户名是否存在
if (StringUtils.isEmpty(account)) {
throw new RuntimeException("获取用户授权信息失败");
}
// 创建一个授权对象
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 进行权限设置
List<String> permissions = account.getPermissions();
if (permissions != null && !permissions.isEmpty()) {
info.addStringPermissions(permissions);
}
// 角色设置
List<String> roles = account.getRoles();
if (roles != null) {
info.addRoles(roles);
}
return info;
}
/**
* 获取用户认证信息
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
throws AuthenticationException {
logger.info("##################执行Shiro登陆认证##################");
StatelessAuthenticationToken statelessAuthenticationToken = (StatelessAuthenticationToken) authenticationToken;
// 通过表单接收的用户名
String token = (String)statelessAuthenticationToken.getPrincipal();
if (StringUtils.isEmpty(token)) {
throw new UnknownAccountException("token无效");
}
// 根据token获取用户信息
LoginAccount account = userService.getAccountByToken(token);
if (account == null) {
throw new UnknownAccountException("token无效");
}
// 创建shiro的用户认证对象
// 注意该对象的密码将会传递至后续步骤与前面登陆的subject的密码进行比对。
//这里放入account对象后面授权可以取出来
//statelessAuthenticationToken会与登录时候的token进行验证这里就放入登录的即可
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(account,
statelessAuthenticationToken, getName());
return authenticationInfo;
}
}
这个类主要用于shiro的登录认证以及权限认证。
2.4、重写shiro的DefaultWebSubjectFactory
重写DefaultWebSubjectFactory主要是关闭创建session
$title(StatelessDefaultSubjectFactory.java)
package net.xqlee.project.demo.shiro.config.shiro;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;
/**
* Subject工厂重写
*
* @author xq
*
*/
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {
@Override
public Subject createSubject(SubjectContext context) {
// 不创建session.
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
2.5 创建一个代理登录的filter
这个filter是无状态的重点之一
$title(StatelessAccessControlFilter.java)
package net.xqlee.project.demo.shiro.config.shiro;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
/**
* 该过滤器需在shiro配置类中加入filter链
*/
public class StatelessAccessControlFilter extends AccessControlFilter {
private static final Logger logger = LoggerFactory.getLogger(StatelessAccessControlFilter.class);
@Value("${token.name}")
String tokenName;
/**
* 先执行:isAccessAllowed 再执行onAccessDenied
* <p>
* isAccessAllowed:表示是否允许访问;mappedValue就是[urls]配置中拦截器参数部分,
* 如果允许访问返回true,否则false;
* <p>
* 如果返回true的话,就直接返回交给下一个filter进行处理。 如果返回false的话,回往下执行onAccessDenied
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
throws Exception {
return false;
}
/**
* onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回true表示需要继续处理;
* 如果返回false表示该拦截器实例已经处理了,将直接返回即可。
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//==============================该步骤主要是通过token代理登录shiro======================
//获取参数中的token值
String token = request.getParameter(tokenName);//这里取的参数中的token你也可以将token放于head等
// 生成无状态Token然后代理登录
StatelessAuthenticationToken statelessAuthenticationToken = new StatelessAuthenticationToken(token);
try {
// 委托给Realm进行登录
getSubject(request, response).login(statelessAuthenticationToken);
} catch (UnknownAccountException ue) {
logger.debug(ue.getLocalizedMessage());
} catch (Exception e) {
logger.error(e.getLocalizedMessage(), e);
//登录失败不用处理后面的过滤器会处理并且能通过@ControllerAdvice统一处理相关异常
}
return true;
}
}
2.6shiro的核心配置
$title(ShiroConfig.java)
package net.xqlee.project.demo.shiro.config.shiro;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.DefaultSessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import net.xqlee.project.demo.shiro.config.shiro.cache.RedisCacheManager;
/***
*
* @author xqlee
*
*/
@Configuration
public class ShiroConfig {
/**
* ehcache缓存方案<br/>
* 简单的缓存,后续可更换为redis缓存,通过自己实现shiro的CacheManager接口和Cache接口
*
* @return
*/
@Bean
public CacheManager shiroEhCacheManager() {
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
return cacheManager;
}
/**
* redis缓存方案
*
* @return
*/
@Bean
public CacheManager shiroRedisCacheManager() {
return new RedisCacheManager();
}
/****
* 自定义Real
*
* @return
*/
@Bean
public UserRealm userRealm() {
UserRealm realm = new UserRealm();
// 根据情况使用缓存器
realm.setCacheManager(shiroRedisCacheManager());// shiroEhCacheManager()
return realm;
}
/**
* 自定义的无状态(无session)Subject工厂
*
* @return
*/
@Bean
public StatelessDefaultSubjectFactory subjectFactory() {
return new StatelessDefaultSubjectFactory();
}
/**
* sessionManager通过sessionValidationSchedulerEnabled禁用掉会话调度器,
* 因为我们禁用掉了会话,所以没必要再定期过期会话了。
*
* @return
*/
@Bean
public DefaultSessionManager sessionManager() {
DefaultSessionManager sessionManager = new DefaultSessionManager();
sessionManager.setSessionValidationSchedulerEnabled(false);
//缓存
sessionManager.setCacheManager(shiroRedisCacheManager());
return sessionManager;
}
/***
* 安全管理配置
*
* @return
*/
@Bean
public SecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 注意这里必须配置securityManager
SecurityUtils.setSecurityManager(securityManager);
// 根据情况选择缓存器
securityManager.setCacheManager(shiroRedisCacheManager());// shiroEhCacheManager()
// 设置Subject工厂
securityManager.setSubjectFactory(subjectFactory());
// 配置
securityManager.setRealm(userRealm());
// session
securityManager.setSessionManager(sessionManager());
// 禁用Session作为存储策略的实现。
DefaultSubjectDAO defaultSubjectDAO = (DefaultSubjectDAO) securityManager.getSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluatord = (DefaultSessionStorageEvaluator) defaultSubjectDAO
.getSessionStorageEvaluator();
defaultSessionStorageEvaluatord.setSessionStorageEnabled(false);
return securityManager;
}
/**
* 配置shiro的拦截器链工厂,默认会拦截所有请求
*
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
// 配置安全管理(必须)
filterFactoryBean.setSecurityManager(defaultWebSecurityManager());
//配置自定义Filter
filterFactoryBean.getFilters().put("statelessAuthc", statelessAuthcFilter());
// 配置登陆的地址
filterFactoryBean.setLoginUrl("/userNoLogin.do");// 未登录时候跳转URL,如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
filterFactoryBean.setSuccessUrl("/welcome.do");// 成功后欢迎页面
filterFactoryBean.setUnauthorizedUrl("/403.do");// 未认证页面
// 配置拦截地址和拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();// 必须使用LinkedHashMap,因为拦截有先后顺序
// statelessAuthc:所有url都必须认无状态证通过才可以访问; anon:所有url都都可以匿名访问
filterChainDefinitionMap.put("/userNoLogin.do**", "anon");// 未登录跳转页面不设权限认证
filterChainDefinitionMap.put("/login.do**", "anon");// 登录接口不设置权限认真
filterChainDefinitionMap.put("/logout.do**", "anon");// 登出不需要认证
// 下面的的其他资源地址全部需要通过代理登录步骤,注意顺序,必须先进过无状态代理登录后,后面的权限和角色认证才能使用
filterChainDefinitionMap.put("/**", "statelessAuthc");
filterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
// 全部配置
// anon org.apache.shiro.web.filter.authc.AnonymousFilter 匿名访问
//
// authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
// 需要登录,不需要权限和角色可访问
//
// authcBasic
// org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
//
// perms
// org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
// 需要给定的权限值才能访问
//
// port org.apache.shiro.web.filter.authz.PortFilter
//
// rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
//
// roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
// 需要给定的角色才能访问
//
// ssl org.apache.shiro.web.filter.authz.SslFilter
//
// user org.apache.shiro.web.filter.authc.UserFilter
//
// logout org.apache.shiro.web.filter.authc.LogoutFilter
return filterFactoryBean;
}
/**
* Add.4.1 访问控制器.
*
* @return
*/
@Bean
public StatelessAccessControlFilter statelessAuthcFilter() {
StatelessAccessControlFilter statelessAuthcFilter = new StatelessAccessControlFilter();
return statelessAuthcFilter;
}
/**
* Add.5.1 开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持;
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* Add.5.2 自动代理所有的advisor: 由Advisor决定对哪些类的方法进行AOP代理。
*/
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
}
三、无状态访问资源创建
3.1 shiro登录
$title(LoginController.java)
package net.xqlee.project.demo.shiro.controller;
import net.xqlee.project.demo.shiro.pojo.LoginAccount;
import net.xqlee.project.demo.shiro.pojo.Result;
import net.xqlee.project.demo.shiro.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 用户登录用
*
* @author xqlee
*/
@RestController
public class LoginController {
private static final Logger logger = LoggerFactory.getLogger(LoginController.class);
@Autowired
UserService userService;
/****
* 用户未登录
*
* @return
*/
@GetMapping("userNoLogin.do")
public Object noLogin() {
return Result.noLogin();
}
@GetMapping(value = "/login.do")
public Object login(String loginName, String password) {
try {
LoginAccount account = userService.getLoginAccountByLoginName(loginName);
if (account == null) {
return Result.fail("账号不存在");
}
if (!account.getPassword().equals(password)) {
return Result.fail("密码错误");
}
if (!account.isEnabled()) {
return Result.fail("账号被锁");
}
String token = userService.createToken(account);
return Result.success(token);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
}
这个类主要是用于登录然后将登录成功的token返回给请求者。
3.2下面就是一个测试的资源访问了
$title(ResourcesController.java)
package net.xqlee.project.demo.shiro.controller;
import net.sf.json.JSONObject;
import net.xqlee.project.demo.shiro.pojo.Result;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
@RestController
public class ResourcesController {
@RequiresPermissions("user:h")//需要用户拥有user:h权限
@GetMapping("/user/hello.do")
public String hello() {
return "Hello User, From Server";
}
@GetMapping("/hello.do")
@RequiresRoles(value = "ROLE_USER")//需要用户拥有ROLE_USER角色
public String hello2() {
return "Hello User2,Form Server";
}
@RequiresPermissions("admin:h")//需要用户拥有admin:h权限
@GetMapping("/admin/hello.do")
public String helloAdmin() {
return "Hello Admin, From Server";
}
@GetMapping("/welcome.do")//
public String loginSuccess() {
return "welcome";
}
@GetMapping("/403.do")
public Object error403(HttpServletResponse response) {
response.setStatus(403);
return Result.noPermission();
}
}
四、测试无状态shiro访问
4.1不登录直接访问
访问无需权限和无需认证的/welcome.do
可以看到成功访问到了无需认证和授权的接口。
接下来访问需要权限的接口:
可以看到返回了用户未登录的信息
4.2 用户登录后访问
首先是用户登录:
可以看到成功返回了token也就是data字段
通过token访问需要普通用户权限的接口:
可以看到上方已经成功访问了需要普通用户权限的接口。
接下来我们访问该用户未有权限的admin相关接口:
上方可以看到,返回了权限不足的提示。
好啦到这里spring boot shiro无状态化已经完成。有何疑问或者更好的建议或者需要项目源码的欢迎留言反馈。
源码下载:点击前往下载源码
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/243611.html