Springboot 整合 Shiro
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
下面我们来看看 Springboot 项目中如何整合使用 Shiro.
整合后项目结构
Springboot 整合 Shiro
1.pom 文件添加 shiro 依赖
<!-- shiro权限依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- shiro ehcache -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
2.自定义身份认证 UserRealm
package com.example.demo.common.shiro;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.demo.common.utils.Cryption;
import com.example.demo.module.user.entity.TblUser;
import com.example.demo.module.user.service.IUserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@Component
public class UserRealm extends AuthorizingRealm {
@Lazy
@Autowired
private IUserService iUserService;
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
TblUser user = (TblUser)principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermission(user.getRole());
return info;
}
/**
* 认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
//查询用户信息
TblUser user = iUserService.getOne(new LambdaQueryWrapper<TblUser>().eq(TblUser::getUserCode,token.getUsername()));
//账号不存在
if(user == null) {
throw new UnknownAccountException("账号或密码不正确");
}
String password = Cryption.genUserPwd(new String((char[]) token.getCredentials()));
System.out.println(password);
if (!user.getPassword().equals(password)) {
// 密码错误
throw new IncorrectCredentialsException("账号或密码不正确");
}
return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
}
/**
* 密码加密算法
* @param credentialsMatcher
*/
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
//密码散列算法
shaCredentialsMatcher.setHashAlgorithmName("MD5");
//散列迭代次数
shaCredentialsMatcher.setHashIterations(1);
super.setCredentialsMatcher(shaCredentialsMatcher);
}
}
3.密码加密工具 Cryption
package com.example.demo.common.utils;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
public class Cryption {
/**
* 生成初始密码
*/
private static String genUserPwd() {
return DigestUtil.md5Hex("888888");
}
/**
* 生成并加密密码
*/
public static String genUserPwd(String pwd) {
if (StrUtil.isEmpty(pwd)) {
return genUserPwd();
}
return DigestUtil.md5Hex(pwd);
}
}
4.shiro 配置 ShiroConfig
package com.example.demo.common.shiro;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import cn.hutool.core.codec.Base64;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
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 配置
*/
@Configuration
public class ShiroConfig {
/**
* 集成缓存
* @return sessionManager
*/
@Bean("sessionManager")
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//设置session过期时间(单位:毫秒),默认为15分钟
sessionManager.setGlobalSessionTimeout(1000 * 60 * 120);
//会话验证调度器
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionIdUrlRewritingEnabled(false);
return sessionManager;
}
@Bean("securityManager")
public SecurityManager securityManager(UserRealm userRealm, SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
// 自定义缓存实现
securityManager.setCacheManager(ehCacheManager());
securityManager.setSessionManager(sessionManager);
// 设置cookie管理
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
/**
* shiro缓存管理器。需要添加到securityManager中
* @return cacheManager
*/
@Bean
public EhCacheManager ehCacheManager(){
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return cacheManager;
}
/**
* Cookie对象
* @return simpleCookie
*/
private SimpleCookie rememberMeCookie() {
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
// 记住时间,单位秒,默认15天
simpleCookie.setMaxAge(60 * 60 * 24 * 15);
return simpleCookie;
}
/**
* cookie管理对象;
* @return cookieRememberMeManager
*/
private CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
// rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
return cookieRememberMeManager;
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//自定义登录页面
shiroFilter.setLoginUrl("/hello");
//自定义未授权页面
shiroFilter.setUnauthorizedUrl("/403");
Map<String, String> filterMap = new LinkedHashMap<>();
/**
* anno:无需认证
* authc: 必须认证才可以访问
* user: 如果使用rememberMe 可以直接访问
* perms: 该资源必须得到资源权限才可以访问
* role: 该资源必须得到角色权限才可以访问
*/
//登出
filterMap.put("/admin/logout", "logout");
//静态资源 开放
filterMap.put("/static/**", "anon");
//登录请求
filterMap.put("/login", "anon");
// /emp/data 需要 emp:data 权限才可访问
filterMap.put("/emp/data", "perms[emp:data]");
//所有请求需认证
filterMap.put("/**","authc");
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
}
shiro 认证过滤器
名称 | 说明 |
---|---|
anno | 无需认证 |
authc | 必须认证才可以访问 |
user | 如果使用rememberMe 可以直接访问 |
perms | 该资源必须得到资源权限才可以访问 |
role | 该资源必须得到角色权限才可以访问 |
5.登录方法 LoginController
package com.example.demo.module.sys.controller;
import com.example.demo.common.controller.BaseController;
import com.example.demo.module.user.entity.TblUser;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
@RestController
public class LoginController extends BaseController {
@PostMapping(value = "login")
public ModelAndView login(@RequestParam("username") String username, @RequestParam("password") String password, boolean rememberMe) {
try {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
subject.login(token);
TblUser user =(TblUser)SecurityUtils.getSubject().getPrincipal();
// SecurityUtils.getSubject().getSession().setAttribute("usercode", user.getUserCode());
} catch (Exception e) {
log.error(e.getMessage(), e);
ModelAndView mv = new ModelAndView("login");
mv.addObject("message",e.getMessage());
return mv;
}
return new ModelAndView("index");
}
}
6.数据准备
在数据库用户表中准备用户数据进行测试
准备了2条用户数据,其中 user1 用户拥有 emp:add 权限,admin 拥有 emp:data 权限。
7.启动项目测试
项目启动后访问项目,因为是未登录状态,会自动跳转到登录页
登录用户 user1
访问 /emp/data, 因为 user1 用户没有 emp:data 权限,所以访问无权限,跳转到403页面。
访问 /emp/2 ,因为该连接只做了登录权限限制,所以 user1 用户可以正常访问。
shiro 注解式开发
shiro提供了相应的注解用于权限控制,如果使用这些注解就需要使用aop的功能来进行判断。shiro提供了spring aop集成,用于权限注解的解析和验证。
想要使用 shiro 的注解,需要在 shiro 配置文件 ShiroConfig
添加如下配置
//管理shiro bean生命周期
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
shiro 注解说明
注解 | 说明 |
---|---|
@RequiresAuthenthentication | 表示当前Subject已经通过login进行身份验证;即 Subject.isAuthenticated()返回 true |
@RequiresUser | 表示当前Subject已经身份验证或者通过记住我登录的 |
@RequiresGuest | 表示当前Subject没有身份验证或者通过记住我登录过,即是游客身份 |
@RequiresRoles(value = {“admin”,“user”},logical = Logical.AND) | 表示当前Subject需要角色admin和user |
@RequiresPermissions(value = {“user:delete”,“user:b”},logical = Logical.OR) | 表示当前Subject需要权限user:delete或者user:b |
EmpController 通过 shiro 注解控制方法权限
package com.example.demo.module.emp.controller;
import com.example.demo.common.dto.R;
import com.example.demo.module.emp.entity.Emp;
import com.example.demo.module.emp.service.IEmpService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* <p>
* 前端控制器
* </p>
*
* @author xx
* @since 2021-05-14
*/
@RestController
@RequestMapping("/emp")
@Api(tags = "用户管理类")
public class EmpController {
@Autowired
private IEmpService iEmpService;
@RequiresPermissions(value = {"emp:data","emp:update"},logical = Logical.OR)
@ApiOperation(value = "用户信息接口")
@GetMapping(value = "/data")
public List<Emp> all(){
return iEmpService.list();
}
@ApiOperation(value = "根据id获取用户信息接口")
@ApiImplicitParam(value = "用户id",name = "id")
@GetMapping(value = "/{id}")
public R getById(@PathVariable(value = "id")Integer id){
return R.ok(iEmpService.getById(id));
}
}
@RequiresPermissions(value = {"emp:data","emp:update"},logical = Logical.OR)
表示 all()
方法需要 emp:data
或 emp:update
权限才可访问
启动项目测试
登录 user1 用户访问
登录 admin 用户访问 /emp/data ,因为 admin 用户拥有 emp:data 权限,所以正常访问
Thymeleaf 扩展 shiro 权限标签
1.pom 文件添加 shiro 标签依赖
<!-- thymleaf 扩展 shiro -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
2.shiro 配置文件 ShiroConfig
添加如下配置开启标签
/**
* 开启shiro标签
*/
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
3.html 添加标签支持
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
4.修改主页 index.html 并让主页可以无登录访问
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>主页</p>
<div>
<shiro:guest>
游客,
<a th:href="@{/hello}">登录</a>
</shiro:guest>
<shiro:user>
<p>
<span shiro:principal property="userCode"></span>
,欢迎您</p>
<a th:href="@{/admin/logout}">退出</a>
</shiro:user>
</div>
<br/>
<br/>
<shiro:user>
<a shiro:hasPermission="emp:data" th:href="@{/emp/data}">/emp/data</a>
<br/>
<a th:href="@{/emp/2}">/emp/2</a>
</shiro:user>
</body>
</html>
5.启动项目测试
未登录状态访问
登录 user1 用户
可以发现,shiro 标签已生效。
贴上 shiro 标签使用说明
<!--验证当前用户是否拥有指定权限。 -->
<a shiro:hasPermission="user:add" href="#" >add用户</a>
<!--与hasPermission标签逻辑相反,当前用户没有制定权限时,验证通过。-->
<p shiro:lacksPermission="user:del"> 没有权限 </p>
<!--验证当前用户是否拥有以下所有权限。-->
<p shiro:hasAllPermissions="user:view, user:add"> 权限与判断 </p>
<!--验证当前用户是否拥有以下任意一个权限。-->
<p shiro:hasAnyPermissions="user:view, user:del"> 权限或判断 </p>
<!--验证当前用户是否属于该角色。-->
<a shiro:hasRole="admin" href="#">拥有该角色</a>
<!--与hasRole标签逻辑相反,当用户不属于该角色时验证通过。-->
<p shiro:lacksRole="developer"> 没有该角色 </p>
<!--验证当前用户是否属于以下所有角色。-->
<p shiro:hasAllRoles="developer, admin"> 角色与判断 </p>
<!--验证当前用户是否属于以下任意一个角色。-->
<p shiro:hasAnyRoles="admin, vip, developer"> 角色或判断 </p>
<!--验证当前用户是否为“访客”,即未认证(包含未记住)的用户。-->
<p shiro:guest="">访客 未认证</a></p>
<!--认证通过或已记住的用户-->
<p shiro:user=""> 认证通过或已记住的用户 </p>
<!--已认证通过的用户。不包含已记住的用户,这是与user标签的区别所在。-->
<p shiro:authenticated=""> <span shiro:principal=""></span> </p>
<!--输出当前用户信息,通常为登录帐号信息-->
<p> <shiro:principal/> </p>
<!--未认证通过用户,与authenticated标签相对应。-->
<!--与guest标签的区别是,该标签包含已记住用户。-->
<p shiro:notAuthenticated=""> 未认证通过用户 </p>
最后贴上整合后的关键代码
pom.xml
<?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.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-test</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- shiro权限依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- thymleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- thymleaf 扩展 shiro -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<!-- 数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 引入 Druid 数据源依赖:https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.9</version>
</dependency>
<!-- mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<!-- mybatis-plus 代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.2</version>
</dependency>
<!-- 代码生成模板-->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.30</version>
</dependency>
<!-- swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--工具-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.5.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
<scope>provided</scope>
</dependency>
<!-- shiro ehcache -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</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>
</plugin>
</plugins>
</build>
</project>
UserRealm
package com.example.demo.common.shiro;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.demo.common.utils.Cryption;
import com.example.demo.module.user.entity.TblUser;
import com.example.demo.module.user.service.IUserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@Component
public class UserRealm extends AuthorizingRealm {
@Lazy
@Autowired
private IUserService iUserService;
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
TblUser user = (TblUser)principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermission(user.getRole());
return info;
}
/**
* 认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
//查询用户信息
TblUser user = iUserService.getOne(new LambdaQueryWrapper<TblUser>().eq(TblUser::getUserCode,token.getUsername()));
//账号不存在
if(user == null) {
throw new UnknownAccountException("账号或密码不正确");
}
String password = Cryption.genUserPwd(new String((char[]) token.getCredentials()));
System.out.println(password);
if (!user.getPassword().equals(password)) {
// 密码错误
throw new IncorrectCredentialsException("账号或密码不正确");
}
return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
}
/**
* 密码加密算法
* @param credentialsMatcher
*/
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
//密码散列算法
shaCredentialsMatcher.setHashAlgorithmName("MD5");
//散列迭代次数
shaCredentialsMatcher.setHashIterations(1);
super.setCredentialsMatcher(shaCredentialsMatcher);
}
}
ShiroConfig
package com.example.demo.common.shiro;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import cn.hutool.core.codec.Base64;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* shiro 配置
*/
@Configuration
public class ShiroConfig {
/**
* 集成缓存
* @return sessionManager
*/
@Bean("sessionManager")
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//设置session过期时间(单位:毫秒),默认为15分钟
sessionManager.setGlobalSessionTimeout(1000 * 60 * 120);
//会话验证调度器
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionIdUrlRewritingEnabled(false);
return sessionManager;
}
@Bean("securityManager")
public SecurityManager securityManager(UserRealm userRealm, SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
// 自定义缓存实现
securityManager.setCacheManager(ehCacheManager());
securityManager.setSessionManager(sessionManager);
// 设置cookie管理
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
/**
* shiro缓存管理器。需要添加到securityManager中
* @return cacheManager
*/
@Bean
public EhCacheManager ehCacheManager(){
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return cacheManager;
}
/**
* Cookie对象
* @return simpleCookie
*/
private SimpleCookie rememberMeCookie() {
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
// 记住时间,单位秒,默认15天
simpleCookie.setMaxAge(60 * 60 * 24 * 15);
return simpleCookie;
}
/**
* cookie管理对象;
* @return cookieRememberMeManager
*/
private CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
// rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
return cookieRememberMeManager;
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
/*重要,设置自定义拦截器,当访问某些自定义url时,使用这个filter进行验证*/
// Map<String, Filter> filters = new LinkedHashMap<>();
// 如果map里面key值为authc,表示所有名为authc的过滤条件使用这个自定义的filter
// map里面key值为LoginFilter,表示所有名为LoginFilter的过滤条件使用这个自定义的filter,具体见下方
// filters.put("myFilter", new LoginFilter());
// shiroFilter.setFilters(filters);
//自定义登录页面
shiroFilter.setLoginUrl("/hello");
//自定义未授权页面
shiroFilter.setUnauthorizedUrl("/403");
Map<String, String> filterMap = new LinkedHashMap<>();
/**
* anno:无需认证
* authc: 必须认证才可以访问
* user: 如果使用rememberMe 可以直接访问
* perms: 该资源必须得到资源权限才可以访问
* role: 该资源必须得到角色权限才可以访问
*/
//登出
filterMap.put("/admin/logout", "logout");
//静态资源 开放
filterMap.put("/static/**", "anon");
//登录请求
filterMap.put("/login", "anon");
filterMap.put("/index", "anon");
// filterMap.put("/emp/data", "perms[emp:data]");
//所有请求需认证
filterMap.put("/**","authc");
// filterMap.put("/**", "myFilter,authc");
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
//管理shiro bean生命周期
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
/**
* 开启shiro标签
*/
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
}
LoginController
package com.example.demo.module.sys.controller;
import com.example.demo.common.controller.BaseController;
import com.example.demo.module.user.entity.TblUser;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
@RestController
public class LoginController extends BaseController {
@PostMapping(value = "login")
public ModelAndView login(@RequestParam("username") String username, @RequestParam("password") String password, boolean rememberMe) {
try {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
subject.login(token);
TblUser user =(TblUser)SecurityUtils.getSubject().getPrincipal();
// SecurityUtils.getSubject().getSession().setAttribute("usercode", user.getUserCode());
} catch (Exception e) {
log.error(e.getMessage(), e);
ModelAndView mv = new ModelAndView("login");
mv.addObject("message",e.getMessage());
return mv;
}
return new ModelAndView("index");
}
}
ShiroUtil
package com.example.demo.common.utils;
import com.example.demo.common.shiro.UserRealm;
import com.example.demo.module.user.entity.TblUser;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.mgt.RealmSecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
/**
* Shiro工具类
*/
public class ShiroUtil {
/**
* 密码散列算法为md5
*/
public final static String HASH_ALGORITHM_NAME = "MD5";
/**
* 散列迭代次数
*/
public final static int HASH_ITERATIONS = 1;
public static String md5(String password) {
return new SimpleHash(HASH_ALGORITHM_NAME, password, HASH_ITERATIONS).toString();
}
public static Session getSession() {
return SecurityUtils.getSubject().getSession();
}
public static Subject getSubject() {
return SecurityUtils.getSubject();
}
public static TblUser getUser() {
return (TblUser)SecurityUtils.getSubject().getPrincipal();
}
public static Integer getUserId() {
return getUser().getId();
}
public static void setSessionAttribute(Object key, Object value) {
getSession().setAttribute(key, value);
}
public static Object getSessionAttribute(Object key) {
return getSession().getAttribute(key);
}
public static boolean isLogin() {
return SecurityUtils.getSubject().getPrincipal() != null;
}
public static void logout() {
SecurityUtils.getSubject().logout();
}
/**
* 刷新权限
*/
public static void flushPrivileges() {
RealmSecurityManager rsm = (RealmSecurityManager) SecurityUtils.getSecurityManager();
UserRealm realm = (UserRealm) rsm.getRealms().iterator().next();
// 只刷新当前用户权限
// realm.clearCachedAuthorization();
// 刷新所有在线用户权限
Cache<Object, AuthorizationInfo> cache = realm.getAuthorizationCache();
if (cache != null) {
for (Object key : cache.keys()) {
cache.remove(key);
}
}
}
}
ExceptHandler
package com.example.demo.common.exception;
import com.example.demo.common.constant.Constants;
import com.example.demo.common.dto.R;
import org.apache.shiro.authz.UnauthorizedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.ModelAndView;
@RestControllerAdvice
public class ExceptHandler {
private final static Logger LOGGER = LoggerFactory.getLogger(ExceptHandler.class);
/**
* 无权限
* @param e
* @return
*/
@ExceptionHandler(UnauthorizedException.class)
public ModelAndView handleUnauthorizedExceptionException(UnauthorizedException e) {
LOGGER.error("error:{}", e.getMessage());
return new ModelAndView("403");
}
@ExceptionHandler(Exception.class)
public R handleException(Exception e) {
LOGGER.error("error:", e);
return R.fail(Constants.OTHER_FAIL_CODE, e.getMessage());
}
}
index.html
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>主页</p>
<div>
<shiro:guest>
游客,
<a th:href="@{/hello}">登录</a>
</shiro:guest>
<shiro:user>
<p>
<span shiro:principal property="userCode"></span>
,欢迎您</p>
<a th:href="@{/admin/logout}">退出</a>
</shiro:user>
</div>
<br/>
<br/>
<shiro:user>
<a shiro:hasPermission="emp:data" th:href="@{/emp/data}">/emp/data</a>
<br/>
<a th:href="@{/emp/2}">/emp/2</a>
</shiro:user>
</body>
</html>
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>登录</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/static/fonts/font-awesome-4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="/static/css/util.css">
<link rel="stylesheet" type="text/css" href="/static/css/main.css">
</head>
<body>
<div class="limiter">
<div class="container-login100">
<div class="wrap-login100">
<div class="login100-form-title" style="background-image: url(/static/images/bg-01.jpg);">
<span class="login100-form-title-1" th:text="#{login.tip}">登 录</span>
</div>
<form class="login100-form validate-form" th:action="@{/login}" method="post">
<span style="color: red;padding: 10px;" th:text="${message}"></span>
<div class="wrap-input100 validate-input m-b-26" th:data-validate="#{login.usernameValidate}">
<span class="label-input100" th:text="#{login.username}">用户名</span>
<label>
<input class="input100" type="text" name="username" th:placeholder="#{login.usernamePlaceholder}"/>
</label>
<span class="focus-input100"></span>
</div>
<div class="wrap-input100 validate-input m-b-18" th:data-validate="#{login.passwordValidate}">
<span class="label-input100" th:text="#{login.password}">密码</span>
<label>
<input class="input100" type="password" name="password" th:placeholder="#{login.passwordPlaceholder}"/>
</label>
<span class="focus-input100"></span>
</div>
<div class="flex-sb-m w-full p-b-30">
<div class="contact100-form-checkbox">
<input class="input-checkbox100" id="ckb1" type="checkbox" name="rememberMe">
<label class="label-checkbox100" for="ckb1" th:text="#{login.remember}">记住我</label>
</div>
<div>
<a href="javascript:" class="txt1" th:text="#{login.forget}">忘记密码?</a>
</div>
</div>
<div class="container-login100-form-btn m-b-18">
<button class="login100-form-btn" th:text="#{login.btn}">登 录</button>
</div>
<div class="flex-sa-m w-full p-b-30">
<div>
<a class="txt1" th:href="@{/hello(l='zh_CN')}">中文</a>
</div>
<div>
<a class="txt1" th:href="@{/hello(l='en_US')}">English</a>
</div>
</div>
</form>
</div>
</div>
</div>
<script src="/static/js/jquery-3.2.1.min.js" type="text/javascript"></script>
<script src="/static/js/main.js" type="text/javascript"></script>
</body>
</html>
ehcache.xml
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
<!--
diskStore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数解释如下:
user.home – 用户主目录
user.dir – 用户当前工作目录
java.io.tmpdir – 默认临时文件路径
-->
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxElementsInMemory="10000"
maxElementsOnDisk="0"
eternal="true"
overflowToDisk="true"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
diskSpoolBufferSizeMB="50"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LFU"/>
<cache name="KACache"
eternal="false"
maxElementsInMemory="100"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="10800"
memoryStoreEvictionPolicy="FIFO"/>
<!--
name:缓存名称。
maxElementsInMemory:缓存最大数目
maxElementsOnDisk:硬盘最大缓存个数。
eternal:对象是否永久有效,一但设置了,timeout将不起作用。
overflowToDisk:是否保存到磁盘,当系统当机时
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
clearOnFlush:内存数量最大时是否清除。
memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
FIFO,first in first out,这个是大家最熟的,先进先出。
LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
-->
</ehcache>
Springboot 整合 Shiro 完成。
内容如有帮助,记得点赞哦~