最近工作需要,研究了下shiro,记录下过程,便于以后回顾,shiro介绍不多说,网上一堆
org.apache.shiro
shiro-spring
1.12.0
org.springframework.boot
spring-boot-starter-data-redis-reactive
org.crazycake
shiro-redis
3.3.1
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroFilterConfiguration {
private final String CACHE_KEY="shiro:cache:";
private final String SESSION_KEY = "shiro:session:";
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.timeout}")
private int timeout;
/**
* 开启注解
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 这个开启shir过滤
* 这个方法主要设置过滤规则
*
*/
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean getShiroFilter(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 自定义拦截器,这里可自己自定义过滤器,当然还可以用自带的几种
Map customFilterMap = new LinkedHashMap<>();
customFilterMap.put("shiroAuthenticationFilter", new ShiroAuthenticationFilter());
shiroFilterFactoryBean.setFilters(customFilterMap);
//shiro登录接口需要跳过过滤器,anon的意思不需要拦截,还有其他好几种
Map map = new HashMap<>();
map.put("/login","anon");
//设置其他接口需要拦截的,使用自定义的过滤器
map.put("/**", "shiroAuthenticationFilter");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 把自定义的一些东西set到DefaultWebSecurityManage中
* 自定义菜单权限,session管理器,缓存管理器等还有其他一些
* 基本很多默认的东西都可以通过继承来重写
*/
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(getMyRealm());
securityManager.setSessionManager(sessionManager());
securityManager.setCacheManager(redisCacheManager());
return securityManager;
}
/**
* MyRealm 中定义了自己去数据库查的菜单和权限放入shiro中
*
* 需要继承AuthorizingRealm
*/
@Bean
public MyRealm getMyRealm() {
MyRealm myRealm = new MyRealm();
return myRealm;
}
/**
* 自定义cookie名称
* sessionId的key可以自定义,默认把sessionid放在cookie中
* 也可以把sessionId放在请求头中,只要登录时返回sessionId就行,然后让前端把sessionId放在头中,都可以自定义。
*/
@Bean
public SimpleCookie sessionIdCookie(){
SimpleCookie cookie = new SimpleCookie("X-Token");
cookie.setMaxAge(-1);
cookie.setPath("/");
cookie.setHttpOnly(true);
return cookie;
}
/**
* 自定义SessionManager管理器
* 通过set一些自定义属性
*
*/
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//自定义cookie 中sessionId 的key
sessionManager.setSessionIdCookie(sessionIdCookie());
sessionManager.setSessionIdCookieEnabled(true);
//删除过期session
sessionManager.setDeleteInvalidSessions(true);
sessionManager.setSessionDAO(redisSessionDAO());
sessionManager.setCacheManager(redisCacheManager());
// 设置全局session过期时间
sessionManager.setGlobalSessionTimeout(timeout*1000);
sessionManager.setSessionValidationSchedulerEnabled(true);
// 取消URL后面的JSESSIONID
sessionManager.setSessionIdUrlRewritingEnabled(false);
return sessionManager;
}
/**
* RedisSessionDAO是操作redis的dao层
*
*
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
redisSessionDAO.setKeyPrefix(SESSION_KEY);
redisSessionDAO.setExpire(timeout);
return redisSessionDAO;
}
/**
* 设置redis的链接配置,地址和账号密码
*
*
*/
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host+":" + port);
redisManager.setTimeout(timeout);
redisManager.setPassword(password);
return redisManager;
}
/**
* 设置生成sessionId的规则
*
* ShiroSessionIdGenerator可自定义规则
*/
@Bean
public ShiroSessionIdGenerator sessionIdGenerator() {
return new ShiroSessionIdGenerator();
}
/**
* 设置缓存管理器,这里缓存也用redis
*
*
*/
@Bean
public RedisCacheManager redisCacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
redisCacheManager.setKeyPrefix(CACHE_KEY);
// 配置缓存的话要求放在session里面的实体类必须有个id标识
redisCacheManager.setPrincipalIdFieldName("ouid");
redisCacheManager.setExpire(timeout);
return redisCacheManager;
}
/**
* Shiro自定义拦截器(需要在 shiroConfig 进行注册)
* 过滤OPTIONS请求,减少会话生成数量
* 继承shiro 的form表单过滤器,对 OPTIONS 请求进行过滤。
* 前后端分离项目中,由于跨域,会导致复杂请求,即会发送preflighted request,这样会导致在GET/POST等请求之前会先发一个OPTIONS请求
* 但 OPTIONS 请求并不带shiro的'authToken'字段(shiro的SessionId),即OPTIONS请求不能通过shiro验证,会返回未认证的信息。
*/
public class ShiroAuthenticationFilter extends FormAuthenticationFilter {
/**
* 直接过滤可以访问的请求类型
*/
private static final String REQUEST_TYPE = "OPTIONS";
public ShiroAuthenticationFilter() {
super();
}
@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (((HttpServletRequest) request).getMethod().equals(REQUEST_TYPE)) {
return true;
}
return super.isAccessAllowed(request, response, mappedValue);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse res = (HttpServletResponse) response;
res.setHeader("Content-type", "text/html;charset=UTF-8");
res.setHeader("Access-Control-Allow-Origin", "*");
res.setContentType("application/json");
res.setStatus(HttpServletResponse.SC_OK);
res.setCharacterEncoding("UTF-8");
PrintWriter writer = res.getWriter();
Map map = new HashMap<>();
map.put("code", 500);
map.put("msg", "未登录");
writer.write(JSON.toJSONString(map));
writer.close();
return false;
}
public class ShiroSessionIdGenerator implements SessionIdGenerator {
private static final String REDIS_PREFIX_LOGIN = "login_token_%s";
@Override
public Serializable generateId(Session session) {
Serializable sessionId = new JavaUuidSessionIdGenerator().generateId(session);
return String.format(REDIS_PREFIX_LOGIN, sessionId);
}
}
public class MyRealm extends AuthorizingRealm {
//操作数据库,从数据库拿权限
@Autowired
private UserRoleSerivce userRoleSerivce;
/**
* 认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String) authenticationToken.getPrincipal();
UserRoleModel userRoleModel = userRoleSerivce.queryUserRoleAuthority(Long.parseLong(username));
AuthenticationInfo info = new SimpleAuthenticationInfo(
userRoleModel,
userRoleModel.getPassword(),
this.getName());
return info;
}
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("======授权 =====");
UserRoleModel userRoleModel = (UserRoleModel) principalCollection.getPrimaryPrincipal();
List roles = new ArrayList();
roles.add(userRoleModel.getRoleName());
List auths = userRoleModel.getAuthorities().stream().map(Authority::getAuthorityName).collect(Collectors.toList());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRoles(roles);
info.addStringPermissions(auths);
return info;
}
}
这个类很重要,菜单角色权限都在这个类中查询数据库set到shiro中
public class ShiroSessionManager extends DefaultWebSessionManager {
//定义常量
private static final String AUTHORIZATION = "Authorization";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
//重写构造器
public ShiroSessionManager() {
super();
this.setDeleteInvalidSessions(true);
}
/**
* 重写方法实现从请求头获取Token便于接口统一
* 每次请求进来,Shiro会去从请求头找Authorization这个key对应的Value(Token)
* @Author Sans
* @CreateTime 2019/6/13 8:47
*/
@Override
public Serializable getSessionId(ServletRequest request, ServletResponse response) {
String token = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
//如果请求头中存在token 则从请求头中获取token
if (!StringUtils.isEmpty(token)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return token;
} else {
// 这里禁用掉Cookie获取方式
// 按默认规则从Cookie取Token
// return super.getSessionId(request, response);
return null;
}
}
}
如果想sessionid不放在cookie中,放在请求头的话,可以使用这个类。
@ControllerAdvice
public class MyShiroException {
/**
* 处理Shiro权限拦截异常
* 如果返回JSON数据格式请加上 @ResponseBody注解
* @Author Sans
* @CreateTime 2019/6/15 13:35
* @Return Map
只要是shiro抛出的异常都统一处理。
@RestController
public class LoginController {
@PostMapping("/login")
public String login(@RequestBody UserRoleModel userRoleModel) {
System.out.println("username=" + userRoleModel.getOuid() + ",password =" + userRoleModel.getPassword());
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(String.valueOf(userRoleModel.getOuid()),userRoleModel.getPassword(),true);
try {
subject.login(token);
return "成功:" + subject.getSession().getId() ;
}catch (UnknownAccountException e){
System.out.println("认证结果: 用户名不正确");
return "认证结果: 用户名不正确";
}catch (IncorrectCredentialsException e){
System.out.println("认证结果:密码不正确 ");
return "认证结果:密码不正确 ";
}
}
@RequestMapping("/getLogout")
@RequiresUser
public Map getLogout(){
//登出Shiro会帮我们清理掉Session和Cache
SecurityUtils.getSubject().logout();
Map map = new HashMap<>();
map.put("code",200);
map.put("msg","登出");
return map;
}
@GetMapping("/index")
public String index() {
return "index";
}
@GetMapping("/auth")
public String auth() {
return "已成功登录";
}
@GetMapping("/role")
@RequiresRoles("admin")
public String role() {
return "测试admin角色";
}
@GetMapping("/permission")
@RequiresPermissions(value = {"0-select", "5-select"}, logical = Logical.AND)
public String permission() {
return "测试Add和Update权限";
}
}
数据库主要有5张表:用户表,角色表,权限表,用户角色关联表,角色权限关联表。
不做过多叙述,表要哪些字段可根据自己需要
shiro还有其他的很多东西可配置,很灵活,还有jsp页面,只不过我这不需要,就没写,其中service,dto操作数据库的代码我就没贴,没必要。
链接:
SpringBoot 整合Shiro实现动态权限加载更新+Session共享+单点登录 - 掘金
Shiro学习文档_shiro中文文档-CSDN博客
SpringBoot集成Shiro极简教程(实战版)_51CTO博客_springboot集成shiro框架