准备工作
-
shiro的相关介绍
三个核心组件:Subject, SecurityManager 和 Realms- Subject: 当前用户或当前用户的安全操作
- SecurityManage: 安全管理器,管理所有用户的安全操作。
- Realms: shiro与项目数据源的交互层(相当于dao层),完成用户的认证(登陆)与鉴权操作。项目中至少存在一个自定义的Realms来继承AuthorizingRealm。
-
引入相关依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!-- shiro ehcache -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
- 创建实体类及相关业务代码
主要实体类为用户表user、角色表role、菜单表menu(包含权限)、及userRole、roleMenu中间表。
基本步骤
- 创建MyRealm,完成相关的认证和鉴权
package com.cm.shiro;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.cm.system.domain.dto.RoleDO;
import com.cm.system.domain.dto.UserDO;
import com.cm.system.service.MenuService;
import com.cm.system.service.RoleService;
import com.cm.system.service.UserService;
import com.cm.utils.CommomUtil;
import org.apache.commons.lang3.StringUtils;
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.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Set;
/**
* 对权限认证和授权的相关操作
* @Author: zlw
* @Date: 2019/7/9 15:21
*/
@Component("myRealm")
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private MenuService menuService;
/**
* 授权
* 在访问目标资源或者目标方法时调用该方法,而不是在认证之后就立即调用
* @param principal 角色
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//1.获取到用户信息,验证用户是否存在
UserDO userDO = (UserDO) principal.getPrimaryPrincipal();
if (null == userDO || null == userDO.getUserId()) {
//如果用户信息为空,则返回空的集合
return null;
}
//2.根据用户信息,添加角色集合和权限集合到info中
List<RoleDO> roles = roleService.findRolesByUserId(userDO.getUserId());
roles.forEach(roleDO -> authorizationInfo.addRole(roleDO.getRoleName()));
Set<String> perms = menuService.listPerms(userDO.getUserId());//根据id查询该用户的所以权限
authorizationInfo.addStringPermissions(perms);
return authorizationInfo;
}
/**
* 登陆验证
* 在登陆接口执行 subject.login(token); 就会调用到该方法
* @param authenticationToken 认证的用户信息
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//0.将token转换为UsernamePasswordToken,获取到用户名
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
// 解决SQL注入的问题
if (StringUtils.isBlank(username) || CommomUtil.sqlInjCheck(username)) {
throw new UnknownAccountException();
}
//1.通过username到数据库查询到用户信息,判断账户状态
EntityWrapper<UserDO> wrapper = new EntityWrapper<>();
wrapper.eq("username", username);
UserDO userDO = userService.selectOne(wrapper);
if (null == userDO) {
//账号不存在
throw new UnknownAccountException();
}
//判断账号状态,可以省略
if (userDO.getStatus() == 0) {
//账号被锁定
throw new LockedAccountException();
}
//2.生成authenticationInfo对象 todo user对象中加入盐的属性,而且返回给前端时jsonIgnore ByteSource.Util.bytes(""),
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
userDO,userDO.getPassword(), ByteSource.Util.bytes("zlwcm"), getName());
return authenticationInfo;
}
}
- shiro的相关配置
package com.cm.shiro;
import com.cm.system.service.MenuService;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Shiro 配置
*/
@Slf4j
@Configuration
public class ShiroConfig {
@Autowired
private RedisProperties redisProperties;
@Autowired
private MenuService menuService;
@Bean
public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
bean.setSuccessUrl("/登陆成功的url");
bean.setLoginUrl("/login");
// 权限过滤 Filter
Map<String, Filter> filterMap = new LinkedHashMap<>(1);
filterMap.put("authc", new CaptchaFormAuthenticationFilter());
bean.setFilters(filterMap);
//todo 配置访问权限
LinkedHashMap<String, String> filterChainDefinitionMap = Maps.newLinkedHashMap();
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/course/list/**", "authc");
bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return bean;
}
/**
* 凭证匹配器(由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了)
* @return HashedCredentialsMatcher
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 使用 MD5 散列算法
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 散列次数
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
/**
* 设置密码加密方式 需与用户注册时对密码加密的方式保持一致。具体操作见工具类Md5Utils
* 否则验证时会报密码不正确异常 IncorrectCredentialsException
* @return
*/
@Bean
public MyRealm authShiroRealm() {
MyRealm authShiroRealm = new MyRealm();
authShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return authShiroRealm;
}
/**
* 安全管理器
* @return
*/
@Bean
public org.apache.shiro.mgt.SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(authShiroRealm());
// 自定义 session 管理,使用 Redis
securityManager.setSessionManager(sessionManager());
// 自定义缓存实现,使用 Redis
securityManager.setCacheManager(cacheManager());
return securityManager;
}
/**
* 自定义 SessionManager
* @return sessionManager
*/
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
/**
* 配置shiro redisManager,使用的是shiro-redis开源插件
*/
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(redisProperties.getHost());
redisManager.setPort(redisProperties.getPort());
redisManager.setPassword(redisProperties.getPassword());
redisManager.setDatabase(redisProperties.getDatabase());
return redisManager;
}
/**
* cacheManager 缓存 redis 实现
*/
@Bean
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
redisSessionDAO.setKeyPrefix("shiro:user:");
return redisSessionDAO;
}
/**
* 开启 Shiro aop 注解支持
* 用代理方式;所以需要开启代码支持
* @param securityManager securityManager
* @return advisor
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
- 登陆调用LoginController
package com.cm.system.controller;
import com.cm.aop.SysLog;
import com.cm.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 登陆 相关操作
*/
@RestController
@Slf4j
public class LoginController {
@SysLog("登陆")
@GetMapping("login")
public R login(@RequestParam("username")String username,@RequestParam("password") String password) {
Subject subject = SecurityUtils.getSubject();
// password = MD5Utils.encrypt(password);
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
String failMsg = "";
try {
subject.login(token);
}catch (UnknownAccountException e) {
failMsg = "用户不存在";
} catch (IncorrectCredentialsException e) {
failMsg = "密码错误!";
} catch (LockedAccountException e) {
failMsg = "登录失败,该用户已被冻结";
} catch (Exception e) {
log.info("系统内部异常!!{}", e);
failMsg = "系统内部异常!!";
}
return failMsg == "" ? R.ok() : R.error(failMsg);
}
}
- 相关工具类
package com.cm.utils;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MD5Utils {
private static final String SALT = "zlwcm";
private static final String ALGORITH_NAME = "md5";
private static final int HASH_ITERATIONS = 2;
private static final Logger LOGGER = LoggerFactory.getLogger(MD5Utils.class);
private MD5Utils() {
super();
}
public static String encrypt(String pswd) {
String newPassword = new SimpleHash(ALGORITH_NAME, pswd, ByteSource.Util.bytes(SALT), HASH_ITERATIONS).toHex();
return newPassword;
}
}
常见异常
- 数据库密码与用户密码不一致:加密方式不一致、用户密码输入或传输转换错误
org.apache.shiro.authc.IncorrectCredentialsException: Submitted credentials for token [org.apache.shiro.authc.UsernamePasswordToken - admin, rememberMe=false] did not match the expected credentials.