在web项目日常开发中,轻量级安全框架shiro主要用来进行认证和授权,可以应用在需要对用户登录、角色、url进行权限控制,也可用应用过滤器进行其他校验。权限控制主要通过用户、角色、权限、用户角色关系、角色权限关系进行维护。
- pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com</groupId>
<artifactId>springboot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.4</version>
<classifier>jdk15</classifier>
</dependency>
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.3</version>
</plugin>
</plugins>
</build>
</project>
- application.properties
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
- ShiroSessionManager
public class ShiroSessionManager extends DefaultWebSessionManager {
/**
* 重写构造器
*/
public ShiroSessionManager() {
super();
this.setDeleteInvalidSessions(true);
}
/**
* 重写方法实现从请求头获取Token便于接口统一
* 每次请求进来,Shiro会去从请求头找REQUEST_HEADER这个key对应的Value(Token)
*/
@Override
public Serializable getSessionId(ServletRequest request, ServletResponse response) {
String token = WebUtils.toHttp(request).getHeader("Authorization");
// 如果请求头中存在token 则从请求头中获取token
if (!ObjectUtils.isEmpty(token)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "Stateless request");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return token;
} else {
// 否则按默认规则从cookie取token
return super.getSessionId(request, response);
}
}
}
-
MyShiroRealm
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 获取当前登录用户信息
SysUser currentUser = (SysUser) principalCollection.getPrimaryPrincipal();
// 根据用户名称去数据库查询用户、角色、权限信息
List<SysRole> roleList = roleService.getRoleListByUserId(currentUser.getId());
for (SysRole role : roleList) {
// 添加角色
simpleAuthorizationInfo.addRole(role.getName());
}
List<SysPermission> permissionList = permissionService.getPermissionListByUserId(currentUser.getId());
for (SysPermission permission : permissionList) {
// 添加权限
simpleAuthorizationInfo.addStringPermission(permission.getPermission());
}
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 获取当前登录用户信息
String name = authenticationToken.getPrincipal().toString();
// 根据用户名称去数据库查询用户
SysUser user = userService.getUserByName(name);
// 根据userId获取role信息
if (user == null) {
//这里返回后会报出对应异常
return null;
} else {
// 这里验证authenticationToken和simpleAuthenticationInfo的信息
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
user,
user.getPwd(),
// 报序列化错误
// ByteSource.Util.bytes(user.getCredentialsSalt()),//salt=username+salt
ByteSourceUtils.bytes(user.getCredentialsSalt()),//salt=username+salt
getName()
);
return simpleAuthenticationInfo;
}
}
public void clearAllCachedAuthorizationInfo() {
getAuthorizationCache().clear();
}
public void clearAllCachedAuthenticationInfo() {
getAuthenticationCache().clear();
}
public void clearAllCache() {
clearAllCachedAuthenticationInfo();
clearAllCachedAuthorizationInfo();
}
}
-
ShiroConfig
@Configuration
public class ShiroConfig {
private final String CACHE_KEY = "shiro:cache:";
private final String SESSION_KEY = "shiro:session:";
// cacheManager、redisSessionDAO 过期时间
private final int EXPIRE = 1800;
@Autowired
private RedisProperties redisProperties;
@Autowired
private PermissionService permissionService;
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 自定义过滤器
Map<String, Filter> filters = new HashMap<>();
// 用户登录状态校验
filters.put("user", new SecurityFormAuthenticationFilter());
// 角色权限校验
filters.put("croles", new SecurityRoleAuthorizationFilter());
// 接口权限校验
filters.put("cperms", new SecurityPermissionAuthorizationFilter());
// 用户退出系统
filters.put("logout", new SecurityLogoutFilter());
shiroFilterFactoryBean.setFilters(filters);
// 自定义过滤器链
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 顺序判断;authc:所有url都必须认证通过才可以访问;anon:所有url都都可以匿名访问
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/getVerifyCode", "anon");
filterChainDefinitionMap.put("/getSid", "anon");
filterChainDefinitionMap.put("/index.html*", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/logout", "logout");
List<PermissionInfo> permissionInfoList = permissionService.getAllRolePermissionList();
// 数据库读取所有角色、权限信息,拦截校验
for (PermissionInfo permissionInfo : permissionInfoList) {
List<SysRole> roleList = permissionInfo.getRoleList();
String roleSet = "";
for (SysRole role : roleList) {
roleSet += role.getName() + ",";
}
filterChainDefinitionMap.put(permissionInfo.getUrl().startsWith("/") ? permissionInfo.getUrl() : "/" + permissionInfo.getUrl(),
"user,authc" + ",cperms[" + permissionInfo.getPermission() + "]" + ("".equals(roleSet) ? "" : ",croles[" + roleSet.substring(0, roleSet.length() - 1) + "]"));
System.out.println("shiro config: url" +
(permissionInfo.getUrl().startsWith("/") ? permissionInfo.getUrl() : "/" + permissionInfo.getUrl()) +
"===" +
("user,authc" + ",cperms[" + permissionInfo.getPermission() + "]" + ("".equals(roleSet) ? "" : ",croles[" + roleSet.substring(0, roleSet.length() - 1) + "]")));
}
filterChainDefinitionMap.put("/user/hello", "user,authc,croles[super]");
filterChainDefinitionMap.put("/**", "user,authc");
// 登录
//shiroFilterFactoryBean.setLoginUrl("/login");
// 首页
//shiroFilterFactoryBean.setSuccessUrl("/index");
// 错误页面,认证不通过跳转
//shiroFilterFactoryBean.setUnauthorizedUrl("/error");
// 将filterChainDefinitionMap放入过滤器链
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 自定义session管理
securityManager.setSessionManager(sessionManager());
// 自定义Cache实现缓存管理
securityManager.setCacheManager(cacheManager());
// 自定义Realm验证
securityManager.setRealm(myShiroRealm());
return securityManager;
}
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
myShiroRealm.setCachingEnabled(true);
myShiroRealm.setAuthenticationCachingEnabled(true);
myShiroRealm.setAuthorizationCachingEnabled(true);
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
// 散列算法:这里使用SHA256算法;
shaCredentialsMatcher.setHashAlgorithmName("md5");
// 散列的次数,比如散列两次,相当于 md5(md5(""));
shaCredentialsMatcher.setHashIterations(2);
return shaCredentialsMatcher;
}
@Bean
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
redisCacheManager.setKeyPrefix(CACHE_KEY);
// 配置缓存的话要求放在session里面的实体类必须有个id标识
redisCacheManager.setPrincipalIdFieldName("id");
// 用户权限信息缓存时间 单位秒 此为30分钟
redisCacheManager.setExpire(EXPIRE);
return redisCacheManager;
}
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setDatabase(redisProperties.getDatabase());
redisManager.setHost(redisProperties.getHost() + ":" + redisProperties.getPort());
// redisManager.setPort(port);
// 若在此设置密码则不能取空密码,需设置redis密码
// redisManager.setPassword(redisProperties.getPassword());
return redisManager;
}
/**
* Redis集群使用RedisClusterManager,单个Redis使用RedisManager
* @return
*/
@Bean
public RedisClusterManager redisClusterManager(){
RedisClusterManager redisClusterManager = new RedisClusterManager();
redisClusterManager.setHost(clusterNodes);
return redisClusterManager;
}
@Bean
public SessionManager sessionManager() {
ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
shiroSessionManager.setSessionDAO(redisSessionDAO());
return shiroSessionManager;
}
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
// redisSessionDAO.setValueSerializer(new ByteSourceSerializer());
redisSessionDAO.setKeyPrefix(SESSION_KEY);
// 此为30分钟
redisSessionDAO.setExpire(EXPIRE);
return redisSessionDAO;
}
@Bean
public ShiroSessionIdGenerator sessionIdGenerator() {
return new ShiroSessionIdGenerator();
}
}
-
SecurityLogoutFilter
public class SecurityLogoutFilter extends LogoutFilter {
private static final Logger log = LoggerFactory.getLogger(SecurityLogoutFilter.class);
@Value("${safe.switch}")
private String safe;
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
Subject subject = getSubject(request, response);
SysUser currentUser = (SysUser) subject.getPrincipal();
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setContentType("application/json; charset=utf-8");
resp.setCharacterEncoding("UTF-8");
String ip = IPUtil.getIpAddress((HttpServletRequest) request);
String url = ((HttpServletRequest) request).getRequestURL() + "";
if (currentUser == null) {
log.info("用户未登录,无法退出系统");
ResponseUtil.returnResultAjax(resp, JSONUtil.object2JSONStr(ResponseData.fail("用户未登录,无法退出系统",400)), null);
} else {
logout(subject);
log.info("用户" + currentUser.getName() + "正常退出!");
if("on".equals(safe)) {
ResponseUtil.returnResultAjax(resp, "退出登录成功", null);
}else {
ResponseUtil.returnResultAjax(resp, "退出登录成功", null);
}
}
return false;
}
public static void logout(Subject subject) {
if (subject == null) {
return;
}
subject.logout();
}
}
- login
@RequestMapping(value = "/login", method = RequestMethod.POST)
public Object login(HttpServletRequest request, HttpServletResponse response, @RequestBody Map<String, String> params) {
String username = params.get("username");
String pwd = params.get("pwd");
String plainPwd = "";
if ("on".equals(safe)) {
//decrypt
plainPwd = pwd;
} else {
plainPwd = pwd;
}
try {
Subject subject = SecurityUtils.getSubject();
System.out.println("登录前sessionId:"+subject.getSession().getId());
// 让旧session失效
subject.logout();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, plainPwd);
// 在此清除认证和授权缓存,需去数据库重新获取用户信息
// myShiroRealm.clearAllCache();
subject.login(usernamePasswordToken);
System.out.println("登陆成功后sessionId:" + subject.getSession().getId());
SysUser user = (SysUser) SecurityUtils.getSubject().getPrincipal();
Map<String, String> returnData = new HashMap<>();
if (user != null) {
returnData.put("userId", String.valueOf(user.getId()));
returnData.put("userName", user.getUserName());
returnData.put("token", subject.getSession().getId().toString());
// sessionId与登录用户的对应关系
subject.getSession().setAttribute(subject.getSession().getId().toString(), returnData);
// 记录登录用户名与IP之间的关系
subject.getSession().setAttribute(user.getName(), IPUtil.getIpAddress(request));
// 为了让请求头中的Cookie失效
Cookie[] cookies= request.getCookies();
if(cookies!=null){
for (Cookie cookie :cookies){
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(0);
cookie.setValue("");
response.addCookie(cookie);
}
}
}
return ResponseData.ok(returnData, "登录成功");
} catch (AuthenticationException ce) {
if (ce instanceof UnknownAccountException) {
return ResponseData.fail("用户不存在", 1001);
} else if (ce instanceof IncorrectCredentialsException) {
return ResponseData.fail("密码错误", 1002);
} else if (ce instanceof LockedAccountException) {
return ResponseData.fail("用户被锁定", 1003);
} else if (ce instanceof ExcessiveAttemptsException) {
return ResponseData.fail("非法尝试", 1004);
} else {
return ResponseData.fail("登录出现异常", 999);
}
}
}
访问测试结果
{
"code": 10019,
"data": null,
"message": "会话过期或未登陆",
"status": false
}
{
"status": false,
"code": 20001,
"message": "验证码不正确或已过期,请重新输入或者刷新验证码",
"data": null
}
{
"code": 19909,
"data": null,
"message": "url越权访问:http://***",
"status": false
}
{
"code": 400,
"data": null,
"message": "用户未登录,无法退出系统",
"status": false
}
{
"status": true,
"code": 200,
"message": null,
"data": "getUser1"
}
注:若将session缓存在redis集群中,须切换RedisClusterManager即可。