最近做一个公司的小项目,使用到shiro做权限管理,在参考几位大佬的博客之后,自己也趟了无数坑,在此做一个记录。此次的springboot版本为:2.1.7.RELEASE。
话不多说,直接代码伺候:
1、shiro部分的pom文件:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<!--session持久化插件-->
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.2.3</version>
</dependency>
2、yml配置文件(个人比较喜欢这种写法)
server:
port: 8080
servlet:
context-path: ######(你的项目路径)
netty:
port: 8899
spring:
datasource:
url: jdbc:mysql://localhost:3306/##(数据库名)?useAffectedRows=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=UTC
username: ####
password: ####
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
http:
multipart:
max-file-size: 10Mb
max-request-size: 10Mb
redis:
shiro:
host: 127.0.0.1
port: 6379
timeout: 0
# password: 123456
mybatis:
mapper-locations: classpath:mybatis/mapper/*.xml
pagehelper:
helperDialect: mysql
reasonable: true #开启优化,<1返回第一页
supportMethodsArguments: true #是否支持接口参数来传递分页参数,默认false
pageSizeZero: false #pageSize=0 返回所有
params: count=countSql
3、shiro的相关配置类
(1)、关于跨域配置的问题,因为项目是前后端完全分离,不可避免会涉及到跨域问题。很奇怪的是,我明明已经配置了跨域,但是在实际运行的时候,还是会出问题,所以添加针对shiro的跨域配置。
/**
* @ClassName ShiroCrosConfig
* @Description shiro跨域问题配置
* @Author Innocence
**/
@Configuration
public class ShiroCrosConfig extends BasicHttpAuthenticationFilter {
@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");//标识允许的请求方法
// 响应首部 Access-Control-Allow-Headers 用于 preflight request (预检请求)中,列出了将会在正式请求的 Access-Control-Expose-Headers 字段中出现的首部信息。修改为请求首部
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
//给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
(2)、关于sessionId获取的方法,我们在使用shiro时,用户认证成功之后会给前端返回一个token,这个token一般是shiro的sessionid。之后前端的每个请求都要带上这个token用于校验。
/**
* @ClassName MySessionManager
* @Description 自定义的session获取
* @Author Innocence
**/
public class MySessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "Authorization";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public MySessionManager(){
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response){
String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
//如果请求头中有AUTHORIZATION,其值为sessionid
if (!StringUtils.isBlank(id)){
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
}
//否则按默认规则从cookie取sessionId
return super.getSessionId(request, response);
}
}
(3)、这里是因为前端使用的是axios发送异步请求,axios在发送真实请求前会先发送一个options预检请求,通过预检才会发送真实请求,所以这里在过滤器里面对options请求进行放行处理。
@Configuration
public class MyFormAuthorizationFilter extends FormAuthenticationFilter {
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) {
HttpServletRequest httpServletRequest = WebUtils.toHttp(servletRequest);
if ("OPTIONS".equals(httpServletRequest.getMethod())) {
return true;
}
return super.isAccessAllowed(servletRequest, servletResponse, o);
}
}
(4)、重头戏来了!!!shiro的核心配置,相关解释在代码注释
@Configuration
public class ShiroConfig {
@Value("${spring.redis.shiro.host}")
private String host;
@Value("${spring.redis.shiro.port}")
private int port;
@Value("${spring.redis.shiro.timeout}")
private int timeout;
// @Value("${spring.redis.shiro.password}")
// private String password;
private Logger logger = LoggerFactory.getLogger(this.getClass());
/*
* 创建ShrioFilterFactoryBean
* @author Innocence
* @date 2019/9/25 002510:57
*/
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边
Map<String, String> filterMap = new LinkedHashMap<>();
/*
* Shiro内置过滤器,可以实现权限相关的拦截器,常用的有:
* anon:无需认证(登录)即可访问
* authc:必须认证才可以访问
* user:如果使用rememberme的功能可以直接访问
* perms:该资源必须得到资源权限才能访问
* role:该资源必须得到角色资源才能访问
*/
// filterMap.put("/admin/**", "authc,roles[超级管理员]");
//放过登录请求
filterMap.put("/user/login", "anon");
// 放过文件上传的接口请求
filterMap.put("/goods/uploadFile","anon");
// filterMap.put("/admin/deleteOne","authc,roles[超级管理员,店长]");
// filterMap.put("/user/getInitMenus","authc");
//放过swagger2
filterMap.put("/swagger-ui.html","anon");
filterMap.put("/swagger-resources", "anon");
filterMap.put("/swagger-resources/configuration/security", "anon");
filterMap.put("/swagger-resources/configuration/ui", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/webjars/springfox-swagger-ui/**", "anon");
filterMap.put("/**", "authc");//其他资源全部拦截
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
// // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
// 配置shiro默认登录地址,前后端分离应该由前端控制页面跳转
shiroFilterFactoryBean.setLoginUrl("/unauth");
return shiroFilterFactoryBean;
}
/*
* 创建DefaultWebSecurityManager
* @author Innocence
* //SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(文档中称之为Subject.)
* @date 2019/9/25 002510:57
*/
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//注入rememberme对象
securityManager.setRememberMeManager(cookieRememberMeManager());
//注入session
securityManager.setSessionManager(sessionManager());
// //注入缓存管理对象
securityManager.setCacheManager(redisCacheManager());
//将Realm注入SecurityManager
securityManager.setRealm(userRealm());
return securityManager;
}
/*
* 创建Realm
* @author Innocence
* @date 2019/9/25 002510:59
*/
@Bean(name = "userRealm")
public UserRealm userRealm(){
UserRealm userRealm = new UserRealm();
//设置解密规则
userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
userRealm.setCachingEnabled(false);
return userRealm;
}
/**
* 因为密码是加过密的,所以,如果要Shiro验证用户身份的话,需要告诉它我们用的是md5加密的,并且是加密了两次。
* 同时我们在自己的Realm中也通过SimpleAuthenticationInfo返回了加密时使用的盐。
* 这样Shiro就能顺利的解密密码并验证用户名和密码是否正确了。
* @author Innocence
* @date 2019/9/27 002716:12
* @return org.apache.shiro.authc.credential.HashedCredentialsMatcher
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//使用MD5散列算法
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//散列两次,相当于MD5(MD5(“”))
hashedCredentialsMatcher.setHashIterations(2);
// storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
/*
* z自定义sessionmanager
* @author Innocence
* @date 2019/10/17 001710:02
* @param []
* @return org.apache.shiro.session.mgt.SessionManager
*/
@Bean(name = "sessionManager")
public SessionManager sessionManager(){
MySessionManager mySessionManager = new MySessionManager();
mySessionManager.setSessionIdUrlRewritingEnabled(false); //取消登陆跳转URL后面的jsessionid参数
mySessionManager.setSessionDAO(redisSessionDAO());
mySessionManager.setGlobalSessionTimeout(-1);//不过期
return mySessionManager;
}
/*
* 配置shiro redisManager
* 使用shiro-redis开源插件
* @author Innocence
* @date 2019/10/17 001710:03
* @param []
* @return org.crazycake.shiro.RedisManager
*/
@Bean(name = "redisManager")
public RedisManager redisManager(){
RedisManager redisManager = new RedisManager();
redisManager.setHost(host+":"+port);
redisManager.setTimeout(timeout);
// redisManager.setPassword(password);
return redisManager;
}
/*
* cacheManager的Redis实现
* @author Innocence
* @date 2019/10/17 001710:20
* @param []
* @return org.crazycake.shiro.RedisCacheManager
*/
@Bean(name = "redisCacheManager")
public RedisCacheManager redisCacheManager(){
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/*
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* 使用shiro-redis开源插件
* @author Innocence
* @date 2019/10/17 001710:23
* @param []
* @return org.crazycake.shiro.RedisSessionDAO
*/
@Bean(name = "redisSessionDAO")
public RedisSessionDAO redisSessionDAO(){
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
/*
* 开启shiro 的aop注解支持
* @author Innocence
* @date 2019/9/27 002716:22
* @param [securityManager]
* @return org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/*
* cookie对象
* @author Innocence
* @date 2019/9/27 002716:24
* @return org.apache.shiro.web.servlet.SimpleCookie
*/
@Bean
public SimpleCookie rememberMeCookie() {
logger.info("ShiroConfiguration.rememberMeCookie()=============");
//这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//<!-- 记住我cookie生效时间30天 ,单位秒;-->
simpleCookie.setMaxAge(259200);
return simpleCookie;
}
/*
* cookie管理对象
* @author Innocence
* @date 2019/9/27 002716:24
* @return org.apache.shiro.web.mgt.CookieRememberMeManager
*/
@Bean
public CookieRememberMeManager cookieRememberMeManager() {
logger.info("ShiroConfiguration.rememberMeManager()========");
CookieRememberMeManager manager = new CookieRememberMeManager();
manager.setCookie(rememberMeCookie());
return manager;
}
/*
* 加入下面两个bean,shiro才会执行授权逻辑
* @author Innocence
* @date 2019/11/6 000616:15
* @param []
* @return org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
}
附上针对shiro的MD5密码加密方法
public String encodePassword(String userName,String passWord){
String hashAlgorithName = "MD5";
int hashIterations = 2;
String password =passWord;
ByteSource credentialsSalt = ByteSource.Util.bytes(userName+"salt");
String obj =String.valueOf(new SimpleHash(hashAlgorithName, password, credentialsSalt, hashIterations));
return obj;
}
4、shiro的自定义realm
public class UserRealm extends AuthorizingRealm {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
@Autowired
private PermissionMapper permissionMapper;
/*
* 执行授权逻辑:因为设计每个用户只有一个角色,所以roleMapper.getRoleByUserName(user)返回值只有一个实体。
* @author Innocence
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
logger.info("自定义UserRealm执行授权逻辑开始==================");
User user =(User) principalCollection.getPrimaryPrincipal();
if (null != user){
List<String> roleLists = new ArrayList<>();
List<String> permissionLists = new ArrayList<>();
Role role = roleMapper.getRoleByUserName(user);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
roleLists.add(role.getRoleName());
info.addRoles(roleLists);
List<Permission> permissionByRoleId = permissionMapper.getPermissionByRoleId(role);
for (Permission permission:permissionByRoleId) {
permissionLists.add(permission.getPermission());
info.addStringPermissions(permissionLists);
}
return info;
}
return null;
}
/*
* 执行认证逻辑
* @author Innocence
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
logger.info("自定义UserRealm执行认证逻辑开始==================");
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
String username = token.getUsername();
char[] password = token.getPassword();
String pass = String.valueOf(password);
User user = new User();
user.setUserName(username);
user.setUserPassWord(pass);
User userInfo = userMapper.getOneUserInfo(user);
if (null == userInfo){
//没有返回登录用户名对应的SimpleAuthenticationInfo对象时,就会在LoginController中抛出UnknownAccountException异常
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
//这里的第一个参数,可以是查询到的用户实体,也可以是用户名。主要是为方便后期Subject的getPrincipal()方法取值。放进去是什么,getPrincipal()取到的就是什么。
userInfo,
//这里的密码,一定是查询到的实体密码,不是参数传递的密码
userInfo.getUserPassWord(),
ByteSource.Util.bytes(userInfo.getUserName()+"salt"),//salt=userName+salt
getName()
);
return authenticationInfo;
}
至此,springboot前后端分离集成shiro基本完成,附上几张经典权限表:
1、用户表
2、角色表
3、权限表
4、用户角色中间表
5、角色权限中间表
接下来,测试用户登录
@RequestMapping("/login")
public Result userLogin(User user){
Result res = new Result();
logger.info("用户登陆请求到达");
// 登录失败从request中获取shiro处理的异常信息。
UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), user.getUserPassWord());
Subject subject = SecurityUtils.getSubject();
try{
subject.login(token);
//从UserRealm里返回的SimpleAuthenticationInfo获取到认证成功的用户名,
//subject.getPrincipal()获取的是SimpleAuthenticationInfo设置的第一个参数
User loginUser = (User) subject.getPrincipal();
Session session = subject.getSession();
session.setAttribute("loginUser" ,loginUser);
// SessionCache.sessionCache.put((String)subject.getSession().getId(),session);
loginUser.setUserPassWord("");
res.put("loginUser",loginUser);
res.put("token",subject.getSession().getId());
res.put("code",ResultCommon.SUCCESS_CODE);
}catch (IncorrectCredentialsException e){
res.put("code",ResultCommon.FAILED_CODE);
res.put("msg","密码错误");
}catch (LockedAccountException e){
res.put("code",ResultCommon.FAILED_CODE);
res.put("msg","账户被冻结");
}catch (AuthenticationException e) {
res.put("code",ResultCommon.FAILED_CODE);
res.put("msg","账户不存在");
} catch (Exception e) {
e.printStackTrace();
}
return res;
}
搞定收工!