Shiro 简介:
Apache Shiro 是 Java 的一个安全框架。目前,使用 Apache Shiro 的人越来越多,因为它相当简单,对比 Spring Security,可能没有 Spring Security 做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。
Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与 Web 集成、缓存等。
更具体的介绍、学习可以看看这个链接:Shiro 简介_w3cschool
这个里面有Shrio的全部学习资料,看完这个之后,在来看Spring Boot 整合Shrio会更加明白,或者Spring Boot 整合Shrio后,再去看看这个文档,也会明白为什么会这么做了。
Spring Boot 的版本:
<version>2.4.10</version>
目录:
1、Spring Boot 与Shrio的简单整合,适合做单体项目
2、Shrio 的会话管理,可以用在前后端分离的项目
3、Shrio 使用redis做缓存
一、Spring Boot 与Shrio的简单整合,适合做单体项目
1、pom.xml
<!--shiro与spring整合需要的包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!--shiro与thymeleaf整合需要的包-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
2、Shrio的配置类型:ShiroConfig
package com.example.springbootshrio.config;
import com.example.springbootshrio.shiro.CustomRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/login.html");//登录页面
shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");//未经授权就可以访问的页面
shiroFilterFactoryBean.setSuccessUrl("/successUrl");//成功页面
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// anon:所有url都都可以匿名访问,一般写静态资源
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/front/**", "anon");
filterChainDefinitionMap.put("/api/**", "anon");
//authc:所有url都必须认证通过才可以访问
filterChainDefinitionMap.put("/admin/**", "authc");
filterChainDefinitionMap.put("/user/**", "authc");
//主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截 剩余的都需要认证
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 创建 SecurityManager 并且绑定 Realm
* @return
*/
@Bean
public org.apache.shiro.mgt.SecurityManager securityManager() {
DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
defaultSecurityManager.setRealm(customRealm());
return defaultSecurityManager;
}
/**
*自定义身份认证realm
*/
@Bean
public CustomRealm customRealm() {
CustomRealm customRealm = new CustomRealm();
return customRealm;
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 开启jsp/html页面的注解
* @return
*/
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
}
3、用户名/密码验证,,以及用户授权
package com.example.springbootshrio.shiro;
import org.apache.shiro.SecurityUtils;
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.subject.SimplePrincipalCollection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* 负责认证用户身份和对用户进行授权
*/
public class CustomRealm extends AuthorizingRealm {
//这里可以注入其他的服务,去查询用户的密码、查询用户的权限等信息
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//用户名
String username = (String) SecurityUtils.getSubject().getPrincipal();
System.out.println("-------根据用户名,获取权限--------");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//权限code,要唯一的,之后的权限验证就是通过匹配它来做的
Set<String> stringPermissions = new HashSet<>();
stringPermissions.add("user:info");
stringPermissions.add("user:update");
info.setStringPermissions(stringPermissions);
//还可以通过角色来做权限验证
Set<String> roles = new HashSet<>();
roles.add("财务经理");
roles.add("普通员工");
info.setRoles(roles);
return info;
}
/**
* 这里可以注入userService,为了方便演示,我就写死了帐号了密码
* private UserService userService;
* <p>
* 用户名和密码的验证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("-------身份认证方法--------");
//用户名
String userName = (String) authenticationToken.getPrincipal();
//密码
String userPwd = new String((char[]) authenticationToken.getCredentials());
//根据用户名从数据库获取密码
String password = "123";
if (userName == null) {
throw new AccountException("用户名或密码不正确");
} else if (!userPwd.equals(password )) {
throw new AccountException("用户名或密码不正确");
}
//如果身份认证验证成功,返回一个AuthenticationInfo实现;
return new SimpleAuthenticationInfo(userName, //用户名
password, //密码
getName() //当前 realm 的名字
);
}
}
4、controller的使用
这里面涉及到的shrio注解,可以参考文档:shiro注解@RequiresPermissions多权限任选一参数用法_qi923701的博客-CSDN博客_requirespermissions的使用
package com.example.springbootshrio.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
@Controller
public class HomeIndexController {
//跳转到登录页面
@GetMapping(value = "/login.html")
public String defaultLogin() {
return "login";
}
@GetMapping(value = "/login")
public String login(@RequestParam("username") String username, @RequestParam("password") String password) {
// 从 SecurityUtils 里边创建一个 subject
Subject subject = SecurityUtils.getSubject();
// 在认证提交前准备 token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 执行认证登陆
try {
subject.login(token);
} catch (UnknownAccountException uae) {
return "未知账户";
} catch (IncorrectCredentialsException ice) {
return "密码不正确";
} catch (LockedAccountException lae) {
return "账户已锁定";
} catch (ExcessiveAttemptsException eae) {
return "用户名或密码错误次数过多";
} catch (AuthenticationException ae) {
return "用户名或密码不正确!";
}
//登录成功,跳转到index页面
if (subject.isAuthenticated()) {
return "index";
} else {
token.clear();
return "登录失败";
}
}
//退出
@RequestMapping(value = "/logout", method = RequestMethod.POST)
@ResponseBody
public void logout() {
// 从 SecurityUtils 里边创建一个 subject
Subject subject = SecurityUtils.getSubject();
//退出
subject.logout();
}
/**
* 通过 代码的if/else 来判断权限,不建议这么使用
* 角色、资源来判断登录用户是否可以访问当前的方法
* @return
*/
@RequestMapping(value = "/getUserInfo", method = RequestMethod.POST)
@ResponseBody
public String getUserInfo(){
Subject subject = SecurityUtils.getSubject();
//根据角色来判断
if (subject.hasRole("财务经理")){
System.out.println("==========登录用户有财务经理的权限,可以进入到这个方法来==================");
return "你有财务经理的权限";
}
//根据资源来判断
try{
subject.checkPermission("user:show");
}catch (AuthorizationException au){
System.out.println("==========登录用户没有当前方法的权限==================");
return "你没有当前方法的权限";
}
System.out.println("==========登录用户有财务经理的权限,可以进入到这个方法来==================");
return "你有财务经理的权限";
}
/**
* 通过 注解来判断权限 建议使用
* @return
*/
@RequiresRoles("财务经理")//角色的注解
@RequiresPermissions(value = {"user:info","user:info"})//资源权限的注解
@RequestMapping(value = "/getRoleInfo", method = RequestMethod.POST)
@ResponseBody
public String getRoleInfo(){
return "你有访问该方法的权限";
}
}
login.html
<form method="get" action="/login">
<table>
<tr>
<td>用户名:</td>
<td>密码:</td>
</tr>
<tr>
<td><input type="text" name="username"></td>
<td><input type="text" name="password"></td>
</tr>
<tr>
<td><input type="submit" value="登录" /></td>
</tr>
</table>
</form>
index.html
这里涉及到的<shiro></shiro>标签,可以参考:
Shiro JSP 标签_w3cschool
<!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>
<script type="text/javascript" src="/jquery.min.js"></script>
</head>
<body>
<!--通过角色来控制页面的权限-->
<shiro:hasRole name="财务经理">
<p>通过角色来控制页面的权限</p>
</shiro:hasRole>
<!--多个角色来控制页面的权限-->
<shiro:hasAndRole name="财务经理,普通员工">
<p>多个角色来控制页面的权限</p>
</shiro:hasAndRole>
<!--通过资源来控制权限-->
<shiro:hasPermission name="user:info">
<p>通过资源来控制权限</p>
</shiro:hasPermission>
<!--多个资源来控制权限-->
<shiro:hasAndPermission name="user:info,user:update">
<p>多个资源来控制权限</p>
</shiro:hasAndPermission>
<button id="_button">点击</button>
<!--点击2,是可以访问到后台的,但是点击退出后,在点击2,那么就不能访问后台了,因为以及退出了。-->
<button id="_button2">点击2</button>
<button id="_button3">退出</button>
</body>
<script>
$("#_button").click(function () {
$.ajax({
type: "post",
url:"/getUserInfo",
success: function (res) {
alert(JSON.stringify(res))
}
});
});
$("#_button2").click(function () {
$.ajax({
type: "post",
url:"/getRoleInfo",
success: function (res) {
alert(JSON.stringify(res))
}
});
});
$("#_button3").click(function () {
$.ajax({
type: "post",
url:"/logout",
success: function (res) {
alert(JSON.stringify(res))
}
});
});
</script>
</html>
index.html的<shrio>标签是用来控制页面权限的,页面必须要引入:
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"
才可以使用<shrio>标签
二、Shrio 的会话管理,可以用在前后端分离的项目
正常来讲 Shiro 是从 Cookie 中获取 SessionId 的,然后找到相对应的 Session 来保证用户登陆的正确性和权限的正确性, 但是在前后端分离的项目中,由于每次的 SessionId 都是不一样的,所以这里选择的是重写 DefaultWebSessionManager 的部分方法, 然后在用户登陆的时候给前端返回 SessionId 来当用户的凭证信息,前端在请求头中携带信息,来解决 Shiro 的用户 Token 认证问题。
1、重写的类:
package com.example.springbootshrio.config;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
/**
* session处理器
*/
public class MySessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "Authorization";
// getReferencedSessionId()方法中的关联源的意思:
// 检测sessionId关联源(
// 如果可以从cookie中获取sessionId,则在request中设置sessionId的关联源为cookie;
// 如果不可以读取,则从request访问路径中获取,如果不存在,则从request的parameter中获取,如果从request的访问路径中或者parameter中获取到的不为空,则设置关联源为url;
// 并在将sessionId和合法信息存储到request中
// )
//sessionId的关联源为 无状态请求
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public MySessionManager() {
super();
}
/**
* 重写获取sessionId的方法调用当前 Manager 的获取方法
* @param request
* @param response
* @return
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
//从请求头获取 sessionId
String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
//如果请求头中有 Authorization 则其值为 sessionId
if (id != null) {
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;
} else {
//否则按默认规则从 cookie 取 sessionId
return super.getSessionId(request, response);
}
}
}
2、配置类:ShiroConfig
/**
* 创建 SecurityManager 并且绑定 Realm
* @return
*/
@Bean
public org.apache.shiro.mgt.SecurityManager securityManager() {
DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
//自定义 realm
defaultSecurityManager.setRealm(customRealm());
// 自定义session管理
defaultSecurityManager.setSessionManager(sessionManager()); //这一行是新增的
return defaultSecurityManager;
}
/**
* shiro session管理
*/
@Bean
public MySessionManager sessionManager() {
MySessionManager sessionManager = new MySessionManager();
return sessionManager;
}
3、使用,controller:用户登录成功后,给用户返回sessionId就可以了
Session session = subject.getSession();
session.getId();
4、前端使用:每次请求时,把后台给的sessionid放在请求头里面给后台就可以了。
$("#_button4").click(function () {
$.ajax({
type: "post",
headers:{"Authorization":token},
url:"/getRoleInfo",
success: function (res) {
alert(JSON.stringify(res))
}
});
});
这里需要注意:session管理器中定义的是从前端的请求头里面获取sessionId,那么前端就需要把sessionId放在头里面,也可以不这么放,可以自定义把sessionId放在其他地方的。
三、Shrio 使用redis做缓存
1、pom.xml,要3.1以上版本的,不然从redis中获取不到数据的。
<!-- shiro+redis缓存插件 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.2.3</version>
</dependency>
2、shrio配置:
/**
* 创建 SecurityManager
* @return
*/
@Bean
public org.apache.shiro.mgt.SecurityManager securityManager() {
DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
//自定义 realm
defaultSecurityManager.setRealm(customRealm());
// 自定义session管理
defaultSecurityManager.setSessionManager(sessionManager());
// 自定义缓存实现 使用redis
defaultSecurityManager.setCacheManager(redisCacheManager());//新增加的行
return defaultSecurityManager;
}
/**
* shiro session管理
*/
@Bean
public MySessionManager sessionManager() {
MySessionManager sessionManager = new MySessionManager();
sessionManager.setSessionDAO(redisSessionDAO());//新增加的行
return sessionManager;
}
/**
* 配置shiro redisManager
* 使用的是shiro-redis开源插件
* @return
*/
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost("127.0.0.1:6379");
redisManager.setTimeout(1800);// 配置缓存过期时间
// redisManager.setPassword(password);//redis密码
return redisManager;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* 使用的是shiro-redis开源插件
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
/**
* 把shrio的缓存换成redis
* cacheManager缓存 redis实现
* 使用的是shiro-redis开源插件
* @return
*/
@Bean
public RedisCacheManager redisCacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
其他的都不行动了,这个时候shiro的缓存,session都是用的redis了。
|