Shrio安全框架
为什么要用Shiro?
1.项目的密码是否可以明文存储?
2.项目中的资源和功能,是否任意访客不用登录都可以访问?
3.项目中的资源和功能,是否任意用户都有权限可以访问?
综上所述,我们需要对用户的操作进行一个安全校验,而Shrio就是一个安全框架,帮助我们做了这些功能。
介绍
Apache Shiro™ 是一个功能强大且易于使用的 Java 安全框架,用于执行身份验证、授权、加密和会话管理。

基本功能:
- 身份验证:有时称为“登录”,这是证明用户就是他们所说的身份的行为。
- 授权:访问控制的过程,即确定“谁”有权访问“什么”。
- 会话管理:即使在非Web或EJB应用程序中,也可以管理用户特定的会话。
- 密码:使用密码算法保持数据安全,同时仍然易于使用。
其他功能:
- Web支持:Shiro的Web支持API可帮助轻松保护Web应用程序。
- 缓存:确保安全操作保持快速有效。
- “记住我”:在整个会话中记住用户的身份,因此他们仅在必要时登录。
架构
高层概述
Shiro主要有三个核心,Subject、SecurityManager、Realm

- Subject:代表当前访问程序的主体,可以是用户也可以是程序,系统调用Subject的API对主体进行认证、授权
- SecurityManager:安全管理器,它管理着所有的Subject以及其他安全配置,主体的认证和授权实际是通过它进行
- Realm:领域,相当于数据源,SecurityManager通过Realm读取主体的认证、授权相关数据
详细架构

- Subject:代表当前访问程序的主体,可以是用户也可以是程序,系统调用Subject的API对主体进行认证、授权
- SecurityManager:安全管理器,它管理着所有的Subject以及其他安全配置,主体的认证和授权实际是通过它进行
- Realm:领域,相当于数据源,SecurityManager通过Realm读取主体的认证、授权相关数据
- Authenticator:登录验证的组件,当用户尝试登录时,该逻辑由Authenticator执行,Authenticator知道如何与存储用户信息的Realm交互,从Realm获取数据验证用户的身份
- Authorizer:决定用户是否有权限访问,它与Authenticator一样,从Realm获取数据验证权限
- SessionManager:创建和管理用户session的生命周期
- CacheManager:CacheManager创建和管理其他Shiro组件使用的Cache实例生命周期。因为这些数据很少改变,放入Cache可以提高性能
- Cryptography:Shiro提供了加密API,可以进行加密解密,比如MD5、SH1
RBAC模型
RBAC:Role Base Access Controller基于角色的访问控制,Shiro的基础原理就是采用安全管理模型
模型中有三个主体:用户、角色、权限
- 每个用户可以有多个角色、每个角色有多个用户
- 每个角色可以有多个权限、每个权限有多个角色
角色对用户和权限做了一个解耦
权限规则
常用的权限标识:【资源 : 操作】
# ":" :作为分割符,分割资源和操作
# "," :作为多个权限的分割符
1.user:add, user:query, user:update, user:delete
# "*":通配符,表示所有资源
2.user:*, *:add
#对于user资源的所有操作
- user:*
#对于所有资源的query操作
- *:query
3.*:代表一切资源
4.细节:
* user:* 可以匹配【user:xx,user:xx:xxx】; *:query只可以匹配【xx:query】,不可以匹配【xx:xx:query】
*【user:insert, user:update, user:delete】可以简写为【"user:insert,update,delete"】,注意要加双引号
实例权限标识别:【资源 : 操作 : 实例】
# 对用户1可以update和delete
user:update:1 , user:delete:1
# 等同于上方
"user:update,delete:1"
URL路径匹配规则
格式是: “url=拦截器[参数],拦截器[参数]”
格式:url=拦截器[参数],拦截器[参数]
url支持Ant风格模式,Ant路径通配符支持?、*、**,通配符匹配不包括"/"
?:匹配一个字符,如 /admin?,匹配/admin1,不匹配/admin和/admin/
*:匹配零个或多个字符,如/admin*,匹配/admin123,/admin,不匹配/admin/123
**:匹配零个或多个路径,如/admin/**,匹配/admin/123
url采取第一次匹配优先的方式,所以对于匹配范围大的路径要放在后面
默认拦截器
Shiro 内置了很多默认的拦截器,比如身份验证、授权等相关的。默认拦截器可以参考org.apache.shiro.web.filter.mgt.DefaultFilter中的枚举拦截器:
public enum DefaultFilter {
anon(AnonymousFilter.class),
authc(FormAuthenticationFilter.class),
authcBasic(BasicHttpAuthenticationFilter.class),
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
perms(PermissionsAuthorizationFilter.class),
port(PortFilter.class),
rest(HttpMethodPermissionFilter.class),
roles(RolesAuthorizationFilter.class),
ssl(SslFilter.class),
user(UserFilter.class);
身份认证相关的
授权相关的
Shiro入门
一个简单的JavaSE程序了解Shiro的API使用
pom.xml
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.26</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
编写ini
shiro支持在配置文件中读取数据,ini文件放在classpath路径下,当然,实际开发中并不会使用ini文件
# 定义用户信息
# 格式:用户=密码,角色1,角色2
[users]
zhangsan = 123,admin
lisi = 456,manager,clerk
wangwu = 789,clerk
# 定义
# 格式:角色=权限1,权限2
[roles]
admin = *
clerk = user:query,user:detail:query
manager = user:*
# 以下这部分,JAVASE测试没用,JAVAEE测试的话可以使用
[main]
#没有身份认证时,跳转的地址
shiro.loginUrl = /user/login
#角色或者权限校验不同时,跳转的地址
shiro.unauthorizedUrl = /author/error
#登出后的跳转地址
shiro.redirectUrl = /user/login
[urls]
# 格式:路径 = 访问方式(anno匿名/authc验证) 范围大的要往后放,因为/** 放在最前面,一旦匹配到就会执行操作了
# anon:访问指定的路径,不需要身份验证,默认就是anno
# authc:访问指定的路径,需要身份验证,如果没有认证,会强制转发到上面配置的loginUrl上
# logout:登出,只需要配置路径,不需要在Controller去编写
# perms["user:all"]:用户需要该权限才能访问,如果没有,会强制转发到上面配置的unauthorizedUrl上
# roles["admin"]:用户需要是该角色才可以访问,如果没有,会强制转发到上面配置的unauthorizedUrl上
/user/login = anon
/user/all = authc,perms["user:all"]
#退出 不用定义请求
/user/logout = logout
#其余路径都需要身份验证
/** = authc
3.Java程序
主要方法:
subject.isAuthenticated():获取用户的登录状态
subject.login(token):用户登录
subject.hasRole("admin"):用户是否属于"admin"角色
sub.isPremitted("user:add"):用户是否有"user:Add"权限
sub.logout():用户退出,清除所有信息
/**
* @User: dingjn
* @Desc: Shiro测试类
*/
public class Shiro {
public static void main(String[] args) {
//读取.ini文件,创建Factory,通过它创建SecurityManager
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//shiro核心 他管理所有的Subject,所有的安全操作都是和它交互的
SecurityManager securityManager = factory.getInstance();
//将securityManager托管到SecurityUtils工具类中,之后就不必再管他
SecurityUtils.setSecurityManager(securityManager);
//通过工具类创建Subject,可以执行Shiro相关操作,它是当前用户,每个请求独享的
Subject subject = SecurityUtils.getSubject();
//身份认证,获取当前用户的登录状态,是否已经登录(ops:从session中同步信息)
System.out.println(subject.isAuthenticated());
if (!subject.isAuthenticated()) {
//参数为Token令牌,设置用户名和密码
AuthenticationToken token = new UsernamePasswordToken("lisi", "456");
//通过subject的login登录,
//如果查询不到用户会抛出UnknownAccountException
//如果用户的密码不正确会抛出IncorrectCredentialsException
try {
subject.login(token);
} catch (UnknownAccountException e) {
System.out.println("用户不存在:" + token.getPrincipal());
return;
} catch (IncorrectCredentialsException e) {
System.out.println("密码错误:" + token.getPrincipal());
return;
} catch (LockedAccountException e) {
System.out.println("账户冻结:" + token.getPrincipal());
} catch (AuthenticationException e) {
System.out.println("其他认证异常:" + e.getMessage());
}
}
//是否有admin角色
System.out.println(subject.hasRole("admin"));
if (subject.hasRole("admin")) {
System.out.println("Hello,管理员");
} else {
System.out.println("Hello,普通用户");
}
//是否有权限 如果没有UnauthorizedException
System.out.println(subject.isPermitted("user:add"));
if (subject.isPermitted("user:add")) {
//正常访问
} else {
System.out.println("Sorry,您没有权限访问");
}
//登出:身份信息、登录状态信息、角色信息、权限信息、会话信息 全部抹除
subject.logout();
System.out.println(subject.isAuthenticated());
System.out.println(subject.getPrincipal());
}
}
Spring集成Shiro
pom.xml
<properties>
<spring.version>5.2.5.RELEASE</spring.version>
</properties>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.4</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.4</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.19</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
</dependencies>
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!--1. 启动Spring容器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!--1.配置DispatcherServlet前端控制器,用于接收请求、拦截请求、分发请求给处理器-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<!--值越小,启动的越早-->
<load-on-startup>1</load-on-startup>
</servlet>
<!--
url-pattern / 和 /*的区别:
/ :匹配所有的请求,不匹配jsp页面
/*:匹配所有的请求,包括jsp页面
-->
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--2.字符编码过滤器-->
<filter>
<filter-name>encoding</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<!--设置编码格式-->
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<!--是否允许上面的编码覆盖已经存在编码方式-->
<init-param>
<param-name>forceEncoding</param-name>
<param-value>false</param-value>
</init-param>
</filter>
<!--
/:不会匹配jsp页面,jsp还是会乱码
/*:也会匹配jsp页面
-->
<filter-mapping>
<filter-name>encoding</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--shiro 过滤器
1.接受所有请求,通过请求路径,识别是否需要安全校验
2.在当前线程中绑定一个subject和SecurityManager,供SecurityUtils使用
DelegatingFilterProxy是Filter的一个代理对象,默认情况下,Spring会到IOC容器中查找和<filter-name>对应的bean,也可以通过targetName定义name
-->
<!-- shiro的filter-->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<!-- shiro的filter-mapping-->
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
spring-mvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--一定要引入spring-shiro.xml-->
<import resource="spring-shiro.xml"></import>
<!--1.扫描controller包-->
<context:component-scan base-package="com.djn.controller"/>
<!--2.让SpringMVC不处理静态资源-->
<mvc:default-servlet-handler/>
<!--3.注解驱动 就可以使用@RequestMapping注解,如果使该注解生效
必须配置HandlerMapping和HandlerAdapter实例
该配置自动完成了以上两个实例的注入
-->
<mvc:annotation-driven/>
<!--4.视图解析器 解析DispatcherServlet传的视图名解析为完整的视图名并返回给它-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!--前缀 最后面的/一定要写!!!!!!!!!!!!!!!!!!!!!-->
<property name="prefix" value="/WEB_INF/jsp/"></property>
<!--后缀-->
<property name="suffix" value=".jsp"/>
</bean>
</beans>
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<import resource="spring-shiro.xml"></import>
</beans>
spring-shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd">
<import resource="spring-dao.xml"></import>
<!-- 对应于web.xml中配置的那个shiroFilter -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- Shiro的核心安全接口,这个属性是必须的 -->
<property name="securityManager" ref="securityManager"/>
<!-- 没有身份认证时,跳转的地址,非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面 -->
<property name="loginUrl" value="/login.jsp"/>
<!-- 登录成功后要跳转的连接(本例中此属性用不到,因为登录成功后的处理逻辑在LoginController里硬编码) -->
<!-- <property name="successUrl" value="/" ></property> -->
<!--用户权限不足或者没有角色使,强制转发到该url-->
<property name="unauthorizedUrl" value="/error/unauthorized"/>
<!--
后面就不会在这里配置了
配置每个路径需要认证才能访问
以及需要访问的权限
格式:url :规则
url支持Ant风格模式,Ant路径通配符支持?、*、**,通配符匹配不包括"/"
?:匹配一个字符,如 /admin?,匹配/admin1,不匹配/admin和/admin/
*:匹配零个或多个字符,如/admin*,匹配/admin123,/admin,不匹配/admin/123
**:匹配零个或多个路径,如/admin/**,匹配/admin/123
url采取第一次匹配优先的方式,所以对于匹配范围大的路径要放在后面
-->
<property name="filterChainDefinitions">
<value>
/login=anon
/login.jsp=anon
<!--退出 不需要再去写方法了-->
/logout=logout
/** = authc
</value>
</property>
</bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"></bean>
<bean id="authorizationAttributeSourceAdvisor"
class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"></property>
</bean>
<bean id="defaultAdvisorAutoProxyCreator"
class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">
<property name="proxyTargetClass" value="true"></property>
</bean>
<!-- 数据库保存的密码是使用MD5算法加密的,所以这里需要配置一个密码匹配对象 -->
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.Md5CredentialsMatcher"></bean>
<!-- 缓存管理 -->
<bean id="shiroCacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"></bean>
<!--
使用Shiro自带的JdbcRealm类
指定密码匹配所需要用到的加密对象
指定存储用户、角色、权限许可的数据源及相关查询语句
-->
<bean id="jdbcRealm" class="com.djn.MyRealm">
<!--d-->
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"></property>
<!--加密次数-->
<property name="hashIterations" value="1024"></property>
</bean>
</property>
</bean>
<!--配置一个bean,该bean实际是一个map-->
<bean id="filterChainDefinitionMap" factory-bean="shiroConfig" factory-method="beanfilterChainDefinitionMap"></bean>
<bean id="shiroConfig" class="com.djn.ShiroConfig"></bean>
<!-- Shiro安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="jdbcRealm"></property>
<property name="cacheManager" ref="shiroCacheManager"></property>
<property name="sessionManager" ref="sessionManager"></property>
<property name="rememberMeManager" ref="rememberMeManager"></property>
</bean>
<!-- 定义RememberMe功能的程序管理类 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<!-- 定义在进行RememberMe功能实现的时候所需要使用到的Cookie的处理类 -->
<property name="cookie" ref="rememberMeCookie"/>
</bean>
<!-- 配置需要向Cookie中保存数据的配置模版(RememberMe) -->
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<!-- 设置Cookie在浏览器中保存内容的名字,由用户自己来设置 -->
<constructor-arg value="TEST-RememberMe"/>
<!-- 保证该系统不会受到跨域的脚本操作供给 -->
<property name="httpOnly" value="true"/>
<!-- 定义Cookie的过期时间为一小时 -->
<property name="maxAge" value="100"/>
</bean>
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="sessionIdUrlRewritingEnabled" value="false"></property>
</bean>
<!-- Shiro的注解配置一定要放在spring-mvc中 -->
</beans>
认证
认证流程:
1.构建SecurityManager环境
2.获取当前的Subject,调用SecurityUtils.getSubject();
2.判断当前用户有没有认证,subject.subject.isAuthenticated()
3.如果没有认证,就将用户的用户名和密码存入Token中
1)用户名和密码由前端传递过来
2)接受参数
4.调用subject.log(token)进行登录认证
5.从Realm中获取用户信息进行验证,并返回,自定义Realm
1) 创建一个类继承AuthenticatingRealm
2)重写doGetAuthenticationInfo方法
6.由Shiro完成用户信息的对比
1.jsp
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="/shiro/login" method="post">
<input type="text" name="username"/>
<input type="text" name="password"/>
<input type="submit" value="登录">
</form>
</body>
</html>
2.controller
@Controller
@RequestMapping("/shiro")
public class ShiroController {
@PostMapping("/login")
public String login(String username, String password) {
Subject subject = SecurityUtils.getSubject();
if (!subject.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token.setRememberMe(true);
subject.login(token);
}
return "redirect:/index.jsp";
}
}
自定义Realm
为什么要自定义Realm呢?
之前我们的用户数据都存储在 ini 文件中,有默认的IniRealm去加载,而我们的实际项目中肯定不会把用户、角色、权限数据存储在文件中,一是不利于管理,二是不灵活
1.如果只需要认证的话,继承AuthenticatingRealm
2.如果需要认证和授权的话,继承AuthorizingRealm
public class MyAuthorizingRealm extends AuthorizingRealm {
/**
* 授权.
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
/**
* 认证.
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
return null;
}
}
3.使用MD5加密,这样前端传递过来的明文密码就会自动加密,当然,也可以不配置,在代码中登录的时候手动加密,在shiro.xml中的Realm中配置
<bean id="myRealm" class="com.djn.MyRealm">
<!--d-->
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!--value也可以使用SH1等加密方式-->
<property name="hashAlgorithmName" value="MD5"></property>
<!--加密次数-->
<property name="hashIterations" value="1024"></property>
</bean>
</property>
</bean>
完整代码:
public class MyRealm extends AuthorizingRealm {
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取用户名
String username = (String) token.getPrincipal();
//去数据库查找信息,这里是模拟登录错误
if ("unknow".equals(username)) {
throw new UnknownAccountException("用户名不存在");
}
if ("moster".equals(username)) {
throw new IncorrectCredentialsException("账户锁定");
}
//模拟数据库中的密码
String credentials = "";
if ("admin".equals(username)) {
//数据库存储的就是加了盐值的,在注册的时候 登录的时候还要加盐
credentials = "038bdaf98f2037b31f1e75b5b4c9b26e";
} else if ("user".equals(username)) {
credentials = "098d2c478e9c11555ce2823231e02ec1";
}
//盐值 使用唯一字符串 保证相同的明文密码加密后也不一样
ByteSource bytes = ByteSource.Util.bytes(username);
//Shiro底层会将token中的密码与它做比对 getName:调用父类方法,获取当前Realm
return new SimpleAuthenticationInfo(username, credentials, bytes, getName());
}
/**
* 授权
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取用户信息
Object principal = principals.getPrimaryPrincipal();
//去数据库查询用户的角色,这里是模拟两个角色
HashSet<String> set = new HashSet<String>();
set.add("user");
if ("admin".equals(principal)) {
set.add("admin");
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(set);
return info;
}
}
授权
Shiro 支持三种方式的授权:
- 编程式:通过写if/else 授权代码块完成
- 注解式:通过在执行的Java方法上放置相应的注解完成,没有权限将抛出相 应的异常(常用)
- JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成

授权流程:
1、首先调用 Subject.isPermitted/hasRole 接口,其会委托给 SecurityManager,而 SecurityManager 接着会委托给 Authorizer;
2、Authorizer是真正的授权者,如果调用如 isPermitted(“user:view”),其首先会通过
PermissionResolver 把字符串转换成相应的 Permission 实例;
3、在进行授权之前,其会调用相应的 Realm 获取 Subject 相应的角 色/权限用于匹配传入的角色/权限;
4、Authorizer 会判断 Realm 的角色/权限是否和传入的匹配,如果 有多个Realm,会委托给 ModularRealmAuthorizer 进行循环判断, 如果匹配如 isPermitted/hasRole 会返回true,否则返回false表示 授权失败。
Shiro标签
Shiro提供了JSTL标签用于JSP权限控制,如根据用户权限不同显示不同的按钮
1.导入shiro
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
2.使用
# <shiro:principal/>显示当前用户名
Welcome:<shiro:principal/> <br>
<!--<shiro:hasRole name="admin"> 具有admin角色才会显示该内容-->
<shiro:hasRole name="admin">
<a href="/admin.jsp">admin.page</a><br>
</shiro:hasRole><br>
<!--<shiro:hasRole name="user"> 具有user角色才会显示该内容-->
<shiro:hasRole name="user">
<a href="/user.jsp"> user.page</a><br>
</shiro:hasRole><br>
<a href="/shiro/logout">logout</a><br>
</body>
权限注解
通过注解来控制权限,可以在Service方法或者Controller方法上添加
注意:如果Service方法有事务@Transcation注解,那就不能添加权限注解,因为@Transcation它会代理一个对象,不能代理一个代理,会发生类型转换异常
@RequieRoles({"admin","user"}):表示需要admin和user角色
@RequirePermissions({"user:add","user,update"}):表示需要user:add和user:update权限
@RequireAuthentication:表示当前用户已经通过了身份认证(不包括记住我)
@RequireUser:表示当前用户已经身份认证或者记住我
@RequireGest:表示当前用户没有身份认证或者记住我,即游客身份
@RequestMapping("/test_annotation")
@RequirePermissions({"user:add","user,update"})
public String testAnnotation() {
System.out.println("testAnnotation");
return "redirect:/index.jsp";
}
异常处理
这里对异常进行一个统一处理
用户名匹配不到会抛出UnknownAccountException;
密码错误会抛出IncorrectCredentialsException;
没有权限会抛出UnauthorizedException
@ControllerAdvice
public class BaseExceptionHandler {
@ExceptionHandler({UnknownAccountException.class})
public String handle(UnknownAccountException e) {
System.out.println("【登录认证】-----> 用户名错误");
return "redirect:/user/login";
}
@ExceptionHandler({IncorrectCredentialsException.class})
public String handle2(IncorrectCredentialsException e) {
System.out.println("【登录认证】-----> 密码错误");
return "redirect:/user/login";
}
@ExceptionHandler({ UnauthorizedException.class})
public String handle2(UnauthorizedException e) {
System.out.println("【访问资源】-----> 权限不足");
return "redirect:/user/login";
}
}
url配置
之前的url配置是在shiro.xml的
中配置,但是如果配置太多就不好管理,可以将配置单独一个类
1.shiro.xml
将之前的filterChainDefinitions去掉,替换为filterChainDefinitionMap
<!-- 对应于web.xml中配置的那个shiroFilter -->
<bean id="abc" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- Shiro的核心安全接口,这个属性是必须的 -->
<property name="securityManager" ref="securityManager"/>
<!-- 没有身份认证时,跳转的地址,非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面 -->
<property name="loginUrl" value="/login.jsp"/>
<!-- 登录成功后要跳转的连接(本例中此属性用不到,因为登录成功后的处理逻辑在LoginController里硬编码) -->
<!-- <property name="successUrl" value="/" ></property> -->
<!--用户权限不足或者没有角色使,强制转发到该url-->
<property name="unauthorizedUrl" value="/error/unauthorized"/>
<!--替换为一个Bean-->
<property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property>
</bean>
<!--配置一个bean,该bean实际是一个map-->
<bean id="filterChainDefinitionMap" factory-bean="shiroConfig" factory-method="beanfilterChainDefinitionMap"></bean>
<bean id="shiroConfig" class="com.djn.ShiroConfig"></bean>
2.ShiroConfig
/**
* @Desc: Shiro统一配置
*/
public class ShiroConfig {
public LinkedHashMap<String, String> beanfilterChainDefinitionMap() {
//注意这里必须是LinkedHashMap,保证有序
LinkedHashMap<String, String> hashMap = new LinkedHashMap<String, String>();
//顺序要弄对,范围大的放在后面,因为会采取最先匹配原则
hashMap.put("/login.jsp", "anon");
hashMap.put("/shiro/logout", "logout");
hashMap.put("/shiro/login", "anon");
hashMap.put("/user.jsp", "roles[admin]");
hashMap.put("/**", "authc");
return hashMap;
}
}
会话管理
Shiro提供了Session会话管理,不依赖底层容器,不管是JAVAEE环境还是JAVASE环境都可以使用,提供了会话持久化、失效/过期支持等
在Controller层还是使用HttpSession,在service层可以直接使用Shiro的session,更加方便
@Controller
@RequestMapping("/shiro")
public class ShiroController {
@Resource
TestService testService;
@RequestMapping("/test_annotation")
@RequiresRoles({"admin"})
public String testAnnotation(HttpSession session) {
session.setAttribute("key", "dingjn");
testService.test();
return "redirect:/index.jsp";
}
}
@Service
public class TestService {
public void test() {
Session session = SecurityUtils.getSubject().getSession();
Object key = session.getAttribute("key");
System.out.println(key);
}
}
记住我
Shiro提供了记住我的功能,当关闭了浏览器以后下次不需要验证,可以直接显示
1.设置路径的过滤器为user
user:当用户第一次开启了rememberMe以后,用户下一次访问如果登录状态还在(也就是Cookie还在)就不需要再次认证,这里的下一次访问指的是用户关闭浏览器后再次打开;比如网站的首页就可以设置为user
authc:无论用户有没有开启rememberMe,下一次访问都需要 重新认证,比如支付,下单就需要重新认证
public class ShiroConfig {
public LinkedHashMap<String, String> beanfilterChainDefinitionMap() {
LinkedHashMap<String, String> hashMap = new LinkedHashMap<String, String>();
hashMap.put("/login.jsp", "anon");
hashMap.put("/shiro/logout", "logout");
hashMap.put("/shiro/login", "anon");
hashMap.put("/user.jsp", "authc,roles[user]");
hashMap.put("/admin.jsp", "authc,roles[admin]");
hashMap.put("/index.jsp", "user"); //设置过滤器为user
hashMap.put("/**", "authc");
return hashMap;
}
}
2.设置时间
我们以前实现记住我是通过设置Cookie的有效时间从而将Cookie持久化本地,Shiro也一样
<!-- 配置需要向Cookie中保存数据的配置模版(RememberMe) -->
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<!-- 设置Cookie在浏览器中保存内容的名字,由用户自己来设置 -->
<constructor-arg value="TEST-RememberMe"/>
<!-- 保证该系统不会受到跨域的脚本操作供给 -->
<property name="httpOnly" value="true"/>
<!-- 定义Cookie的过期时间为一小时 -->
<property name="maxAge" value="100"/>
</bean>
<!-- 定义RememberMe功能的程序管理类 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<!-- 定义在进行RememberMe功能实现的时候所需要使用到的Cookie的处理类 -->
<property name="cookie" ref="rememberMeCookie"/>
</bean>
<!--避免url携带jessionId-->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="sessionIdUrlRewritingEnabled" value="false"></property>
</bean>
<!-- Shiro安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!--Realm配置-->
<property name="realm" ref="jdbcRealm"></property>
<!--缓存配置-->
<property name="cacheManager" ref="shiroCacheManager"></property>
<!--Session配置-->
<property name="sessionManager" ref="sessionManager"></property>
<!--记住我配置-->
<property name="rememberMeManager" ref="rememberMeManager"></property>
</bean>
3.登录设置
在登录时将rememberMe设置为true,实际开发中根据前端传过来的checkbox值来判断
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token.setRememberMe(true);
subject.login(token);
SpringBoot集成Shiro
Todo.

浙公网安备 33010602011771号