文章目录
1. 下载
- 官网地址
- 选择Source Code Distribution,下载,解压
2. 测试
实际开发使用注解的方式,搭建web开发环境,不会在ini配置文件进行硬编码的操作
- 测试样例参考
shiro解压目录\samples\quickstart
- 需要的jar包
shiro-all.jar
log4j.jar
slf4j-api.jar
slf4j-log4j12.jar
- 官方shiro.ini文件(截取部分文件)
[users]
# 用户 'root' 密码 'secret' 角色 'admin'
root = secret, admin
# 用户 'guest' 密码 'guest' 角色 'guest'
guest = guest, guest
# 用户 'darkhelmet' 密码 'ludicrousspeed' 角色 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# 用户名 = 类型-用户:操作-删除:实例-张三--->goodgay 可以删除张三的用户信息
# 主:谓:宾
goodguy = user:delete:zhangsan
- 官方Quickstart.java:\samples\quickstart\src\main\java\Quickstart.java
public class Quickstart {
private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);
public static void main(String[] args) {
// 使用ini配置文件配置realms, users, roles and permissions,创建Shiro SecurityManager
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
/**
* 如何操作shiro 模拟用户、密码、角色、权限都在ini配置文件里
*/
// 1. 获取当前的 Subject. 调用 SecurityUtils.getSubject();
Subject currentUser = SecurityUtils.getSubject();
// 获取 Session: Subject#getSession()
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("---> Retrieved the correct value! [" + value + "]");
}
// 2. 测试当前的用户是否已经被认证. 即是否已经登录.
// 调动 Subject 的 isAuthenticated()
if (!currentUser.isAuthenticated()) {
// 把用户名和密码封装为 UsernamePasswordToken 对象
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
// rememberme
token.setRememberMe(true);
try {
// 执行登录.
currentUser.login(token);
}
// 若没有指定的账户, 则 shiro 将会抛出 UnknownAccountException 异常.
catch (UnknownAccountException uae) {
log.info("----> There is no user with username of " + token.getPrincipal());
return;
}
// 若账户存在, 但密码不匹配, 则 shiro 会抛出 IncorrectCredentialsException 异常。
catch (IncorrectCredentialsException ice) {
log.info("----> Password for account " + token.getPrincipal() + " was incorrect!");
return;
}
// 用户被锁定的异常 LockedAccountException
catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// 所有认证时异常的父类.
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}
//say who they are:
//print their identifying principal (in this case, a username):
log.info("----> User [" + currentUser.getPrincipal() + "] logged in successfully.");
// 测试是否有某一个角色. 调用 Subject 的 hasRole 方法.
if (currentUser.hasRole("schwartz")) {
log.info("----> May the Schwartz be with you!");
} else {
log.info("----> Hello, mere mortal.");
return;
}
// 测试用户是否具备某一个行为. 调用 Subject 的 isPermitted() 方法。
if (currentUser.isPermitted("lightsaber:weild")) {
log.info("----> You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
// 测试用户是否具备某一个行为.
if (currentUser.isPermitted("user:delete:zhangsan")) {
log.info("----> You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}
// 执行登出. 调用 Subject 的 Logout() 方法. 通过测试用户是否被认证来查看用户是否登陆退出
System.out.println("---->" + currentUser.isAuthenticated());
currentUser.logout();
System.out.println("---->" + currentUser.isAuthenticated());
System.exit(0);
}
}
3. SSM整合教程
3.1 基本认证流程
- 获取当前的 Subject,调用 SecurityUtils.getSubject()
- 测试当前的用户是否已经被认证. 即是否已经登录,调用 Subject 的 isAuthenticated() 方法
- 如果没有被认证,则把用户名和密码(通过将表单信息提交到SpringMVC中)封装为 UsernamePasswordToken 对象
- 执行登录,调用 Subject 的 login(token) 方法
- 自定义Realm,继承 AuthenticatingRealm,实现 doGetAuthenticationInfo(),通过数据库获取对应记录,返回给shiro
- 通过shiro完成密码比对:前台输入的 UsernamePasswordToken 与 数据库查询的 SimpleAuthenticationInfo 信息进行比对
3.2 基本配置
- 依赖管理:修改pom.xml
<!-- 添加shiro支持 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.4</version>
</dependency>
- 修改web.xml
<!-- shiro过滤器定义 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<!-- 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理 -->
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
- 新建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.xsd">
<!-- 1.配置 SecurityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager" />
<property name="realm" ref bean="jdbcRealm" />
<property name="rememberMeManager.cookie.maxAge" value="10"></property>
</bean>
<!-- 1.1配置缓存管理器,需要加入 ehcache 的 jar 包及配置文件 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
</bean>
<!-- 1.2配置 Realm 直接配置实现了 org.apache.shiro.realm.Realm 接口的 bean -->
<bean id="jdbcRealm" class="com.cc.shiro.realms.ShiroRealm"></bean>
<!-- 2.启用shiro注解 -->
<!-- 2.1配置 LifecycleBeanPostProcessor. 可以自定的来调用配置在 Spring IOC 容器中 shiro bean 的生命周期方法 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- 2.2启用 IOC 容器中使用 shiro 的注解. 但必须在配置了 LifecycleBeanPostProcessor 之后才可以使用 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
<!-- 3.配置 ShiroFilter -->
<!--
id 必须和 web.xml 文件中配置的 DelegatingFilterProxy 的 <filter-name> 一致.
若不一致, 则会抛出: NoSuchBeanDefinitionException. 因为 Shiro 会来 IOC 容器中查找和 <filter-name> 名字对应的 filter bean.
-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.jsp"/>
<property name="successUrl" value="/success.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/>
<!-- <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property> -->
<!--
配置哪些页面需要受保护. /** 使用通配符表示其他所有页面
以及访问这些页面需要的权限.
1). anon 可以被匿名访问
2). authc 必须认证(即登录)后才可能访问的页面.
3). logout 登出.
4). roles 角色过滤器
格式:url=拦截器
通配符:?表示一个字符,*表示0个或多个字符,**表示0个或多个路径
-->
<property name="filterChainDefinitions">
<value>
<!-- 允许匿名访问 -->
/login.jsp = anon
/shiro/login = anon
/shiro/logout = logout
# everything else requires authentication:
/** = authc
</value>
</property>
</bean>
</beans>
- 在spring.xml文件中导入spring-shiro.xml
<import resource="spring-shiro.xml"/>
- 自定义Realm
public class ShiroRealm extends AuthenticatingRealm{
/**
* token:是SpringMVC从请求中获取的
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("token.hashCode() = " + token.hashCode());
//1. 把 AuthenticationToken 转换为 UsernamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//2. 从 UsernamePasswordToken 中来获取 username
String username = upToken.getUsername();
//3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
System.out.println("用户名:"+username);
//4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
if("unknown".equals(username)){
throw new UnknownAccountException("用户不存在!");
}
//5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常
if("locked".equals(username)){
throw new LockedAccountException("用户被锁定!");
}
//6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
//1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象
Object principal = username;
//2). credentials: 从数据库中获取密码,以下是在本地模拟
Object credentials = "123456";
//3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
String realmName = getName();
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, realmName);
return info;
}
}
- login.jsp
<form action="shiro/login" method="POST">
username: <input type="text" name="username"/>
<br><br>
password: <input type="password" name="password"/>
<br><br>
<input type="submit" value="Submit"/>
</form>
- success.jsp
<a href="shiro/logout">Logout</a>
- ShiroController
@Controller
@RequestMapping("/shiro")
public class ShiroController {
@RequestMapping("/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password){
Subject currentUser = SecurityUtils.getSubject();
if (!currentUser.isAuthenticated()) {
// 把用户名和密码封装为 UsernamePasswordToken 对象
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// rememberme
token.setRememberMe(true);
try {
System.out.println("token.hashCode() = " + token.hashCode());
// 执行登录
currentUser.login(token);
}
// ... catch more exceptions here (maybe custom ones specific to your application?
// 所有认证时异常的父类
catch (AuthenticationException ae) {
//unexpected condition? error?
System.out.println("登录失败: " + ae.getMessage());
}
}
return "redirect:/success.jsp";
}
}
- 测试效果
- 测试访问其他页面:无法访问,返回login.jsp
- 测试用户名输入:unknown,打印:登录失败: 用户不存在!
- 测试输入用户名:aa、密码:123456(即定义的Realm中的密码),登陆成功
3.3 密码加密配置
- 将前台获取的密码进行加密:修改 spring-shiro.xml 配置的 Realm
<bean id="jdbcRealm" class="com.cc.shiro.realms.ShiroRealm">
<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>
- 修改自定义Realm
public class ShiroRealm extends AuthenticatingRealm{
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//前面不变..
//6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
//1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象
Object principal = username;
//2). credentials: 密码
Object credentials = "038bdaf98f2037b31f1e75b5b4c9b26e"; //123456采用MD5加密后的结构
//3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
String realmName = getName();
//4). 盐值
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
return info;
}
}
- 原理
//shiro-core.jar包下
package org.apache.shiro.authc;
public class UsernamePasswordToken {
public char[] getPassword() {
return password; // 打断点
}
}
//不断下一步,会来到equals方法,进行密码比对
package org.apache.shiro.authc.credential;
public class HashedCredentialsMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenHashedCredentials = hashProvidedCredentials(token, info);
Object accountCredentials = getCredentials(info);
return equals(tokenHashedCredentials, accountCredentials);
}
}
3.4 配置多个Realm
- 增加新的自定义的Realm,使用SHA1加密,其他不变
public class ShiroRealm2 extends AuthenticatingRealm{
/**
* token:是SpringMVC从请求中获取的
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("token.hashCode() = " + token.hashCode());
System.out.println("ShiroRealm2");
//1. 把 AuthenticationToken 转换为 UsernamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//2. 从 UsernamePasswordToken 中来获取 username
String username = upToken.getUsername();
//3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
System.out.println("用户名:"+username);
//4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
if("unknown".equals(username)){
throw new UnknownAccountException("用户不存在!");
}
//5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常
if("locked".equals(username)){
throw new LockedAccountException("用户被锁定!");
}
//6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
//1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象
Object principal = username;
//2). credentials: 密码
Object credentials = "ce2f6417c7e1d32c1d81a797ee0b499f87c5de06"; //123456
//3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
String realmName = getName();
//4). 盐值
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
return info;
}
}
- 修改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.xsd">
<!-- 1.配置 SecurityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="authenticator" ref="authenticator"></property>
<property name="rememberMeManager.cookie.maxAge" value="10"></property>
<!-- 尽管可以放在配置认证策略中,但很多时候需要直接从securityManager获取realms -->
<property name="realms">
<list>
<ref bean="firstRealm"/>
<ref bean="secondRealm"/>
</list>
</property>
</bean>
<!-- 1.1配置缓存管理器,需要加入 ehcache 的 jar 包及配置文件 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
</bean>
<!-- 1.2配置认证策略(针对多个Realm) -->
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"></bean>
</property>
</bean>
<!-- 1.3配置 Realm 直接配置实现了 org.apache.shiro.realm.Realm 接口的 bean -->
<bean id="firstRealm" class="com.cc.shiro.realms.ShiroRealm">
<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 id="secondRealm" class="com.cc.shiro.realms.ShiroRealm2">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!-- 指定加密算法 -->
<property name="hashAlgorithmName" value="SHA1"></property>
<!-- 指定加密次数 -->
<property name="hashIterations" value="1024"></property>
</bean>
</property>
</bean>
<!--其余部分未修改 -->
</beans>
- 原理
//controller
currentUser.login(token);
//调用Subject接口的login方法
public interface Subject {
void login(AuthenticationToken token) throws AuthenticationException;
}
//去找login的实现方法,这里调用securityManager.login()方法
public class DelegatingSubject implements Subject {
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
Subject subject = securityManager.login(this, token);
//...
}
//来到SecurityManager接口
public interface SecurityManager extends Authenticator, Authorizer, SessionManager {
Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;
}
//找对应实现类,调用authenticate()方法
public class DefaultSecurityManager extends SessionsSecurityManager {
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token);
}
//...
}
}
//继续进入
public abstract class AuthenticatingSecurityManager extends RealmSecurityManager {
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
}
//找authenticate()方法实现类,这里执行doAuthenticate()方法
public abstract class AbstractAuthenticator implements Authenticator, LogoutAware {
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
//...
AuthenticationInfo info;
try {
info = doAuthenticate(token);
//...
}
}
//找doAuthenticate()实现方法
public abstract class AbstractAuthenticator implements Authenticator, LogoutAware {
protected abstract AuthenticationInfo doAuthenticate(AuthenticationToken token)
throws AuthenticationException;
}
//ModularRealmAuthenticator.class中getRealms()获取到的是Realm的集合
public class ModularRealmAuthenticator extends AbstractAuthenticator {
//263行
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
//单个Realms认证
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
//多个Realms认证
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
//198行
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
//选择认证策略
AuthenticationStrategy strategy = getAuthenticationStrategy();
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
//...
}
3.5 授权
- 增加user.jsp
- 增加admin.jsp
- 修改success.jsp
<body>
<h1>成功!</h1>
<a href="shiro/logout">Logout</a>
<a href="user.jsp">User Page</a>
<a href="admin.jsp">Admin page</a>
</body>
- 修改自定义Realm:模拟分别让user、admin登陆系统,密码都是123456
public class ShiroRealm extends AuthenticatingRealm{
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//...
Object credentials = null;
if("admin".equals(username)){
credentials = "038bdaf98f2037b31f1e75b5b4c9b26e";//123456加盐:admin、加密:MD5
}else if("user".equals(username)){
credentials = "098d2c478e9c11555ce2823231e02ec1";//123456加盐:user、加密:MD5
}
//...
}
}
- 测试:登陆(用户名:user、密码:123456),登陆成功,可以访问user.jsp、admin.jsp
- 修改spring-shiro.xml,设置user.jsp、admin.jsp的访问权限
<property name="filterChainDefinitions">
<value>
<!-- 允许匿名访问 -->
/login.jsp = anon
/shiro/login = anon
/shiro/logout = logout
/user.jsp = roles[user]
/admin.jsp = roles[admin]
# everything else requires authentication:
/** = authc
</value>
</property>
</bean>
</beans>
- 测试:登陆(用户名:user、密码:123456),登陆成功,不可以访问user.jsp、admin.jsp,会跳转到unauthorized.jsp
- 通过以下原理分析,得出自定义Realm需要实现AuthorizingRealm,之前实现的是AuthenticatingRealm,所以修改ShiroRealm.java
public class ShiroRealm extends AuthorizingRealm{
/**
* 1.认证方法
* token:是SpringMVC从请求中获取的
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("token.hashCode() = " + token.hashCode());
//1. 把 AuthenticationToken 转换为 UsernamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//2. 从 UsernamePasswordToken 中来获取 username
String username = upToken.getUsername();
//3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
System.out.println("用户名:"+username);
//4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
if("unknown".equals(username)){
throw new UnknownAccountException("用户不存在!");
}
//5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常
if("locked".equals(username)){
throw new LockedAccountException("用户被锁定!");
}
//6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
//1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象
Object principal = username;
//2). credentials: 从数据库中获取密码,以下是在本地模拟
Object credentials = null;
if("admin".equals(username)){
credentials = "038bdaf98f2037b31f1e75b5b4c9b26e";//123456加盐:admin、加密:MD5
}else if("user".equals(username)){
credentials = "098d2c478e9c11555ce2823231e02ec1";//123456加盐:user、加密:MD5
}
//3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
String realmName = getName();
//4). 盐值
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
return info;
}
/**
* 2.授权会被shiro回调的方法
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//1. 从 PrincipalCollection 中来获取登录用户的信息
Object principal = principals.getPrimaryPrincipal();
//2. 利用登录用户的信息获取当前用户的角色或权限(可能需要查询数据库)
Set<String> roles = new HashSet<>();
roles.add("user"); // 模拟:所有用户都被赋予user角色
if("admin".equals(principal)){
roles.add("admin"); // 模拟:只有admin登陆后才有admin角色
}
//3. 创建 SimpleAuthorizationInfo, 并设置其 reles 属性.
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
//4. 返回 SimpleAuthorizationInfo 对象.
return info;
}
}
- 测试
- 登陆【user、123456】,只能访问user.jsp
- 登陆【admin、123456】,能访问user.jsp、admin.jsp
- 原理(分析得出,自定义Realm需要实现AuthorizingRealm的doGetAuthorizationInfo方法)
//判断是否有权限:调用Subject.class下的hasRole()方法
public interface Subject {
boolean hasRole(String roleIdentifier);
}
//hasRole()的实现方法:调用securityManager的hasRole()方法
public class DelegatingSubject implements Subject {
public boolean hasRole(String roleIdentifier) {
return hasPrincipals() && securityManager.hasRole(getPrincipals(), roleIdentifier);
}
}
//执行AuthorizingSecurityManager.class下的hasRole()方法
public abstract class AuthorizingSecurityManager extends AuthenticatingSecurityManager {
public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
return this.authorizer.hasRole(principals, roleIdentifier);
}
}
//来到ModularRealmAuthorizer.class下的hasRole()方法
public class ModularRealmAuthorizer implements Authorizer, PermissionResolverAware, RolePermissionResolverAware {
public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
assertRealmsConfigured();//验证realms不为空
for (Realm realm : getRealms()) {
if (!(realm instanceof Authorizer)) continue;
if (((Authorizer) realm).hasRole(principals, roleIdentifier)) {
return true;
}
}
return false;
}
}
//这里将realm强转为Authorizer,去调用Authorizer接口下的hasRole()方法
public interface Authorizer {
boolean hasRole(PrincipalCollection subjectPrincipal, String roleIdentifier);
}
//hasRole()方法的具体实现方法为AuthorizingRealm.class下的hasRole()方法
public abstract class AuthorizingRealm extends AuthenticatingRealm
implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware {
//572行,hasRole()方法执行getAuthorizationInfo
public boolean hasRole(PrincipalCollection principal, String roleIdentifier) {
AuthorizationInfo info = getAuthorizationInfo(principal);
return hasRole(roleIdentifier, info);
}
//310行,执行doGetAuthorizationInfo
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
//...
if (info == null) {
info = doGetAuthorizationInfo(principals);
//...
}
}
//399行,该方法是一个抽象方法,需要被实现【重要!】
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);
//577行,执行hasRole
protected boolean hasRole(String roleIdentifier, AuthorizationInfo info) {
return info != null && info.getRoles() != null && info.getRoles().contains(roleIdentifier);
}
}
3.6 权限注解
@RequiresAuthentication
表示当前Subject已经通过login 进行了身份验证;即Subject. isAuthenticated()返回true。
@RequiresUser
表示当前Subject已经身份验证或者通过记住我登录的。
@RequiresGuest
表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。
@RequiresRoles(value={“admin”, “user”}, logical= Logical.AND)
@RequiresRoles(value={“admin”})
@RequiresRoles({“admin“})
表示当前Subject需要角色admin 和user。
@RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR)
表示当前Subject需要权限user:a或user:b。
3.6.1 不使用@Service注解
public class ShiroService {
@RequiresRoles({"admin"})
public void testMethod(){
System.out.println("testMethod, time: " + new Date());
}
}
spring.xml中将ShiroService添加到IOC容器
<bean id="shiroService" class="com.cc.shiro.service.ShiroService"></bean>
测试注解使用:必须角色admin才能执行testMethod();如果以【user、123456】登陆,无法执行testMethod(),会报错Subject does not have role [admin]
;以【admin、123456】登陆,可以执行testMethod()
3.6.2 使用@Service注解
坑:shiro注解不生效原因:官方说明shiro和spring集成时,spring-shiro.xml需要被spring.xml引用,但是集成springmvc时,spring-shiro.xml中关于开启注解的部分需要声明在spring-mvc.xml中
<!-- 2.启用shiro注解:将这部分放在spring-mvc.xml下 -->
<!-- 2.1配置 LifecycleBeanPostProcessor. 可以自定的来调用配置在 Spring IOC 容器中 shiro bean 的生命周期方法 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- 2.2启用 IOC 容器中使用 shiro 的注解. 但必须在配置了 LifecycleBeanPostProcessor 之后才可以使用 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true" />
</bean>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
@Service
public class ShiroService {
@RequiresRoles({"admin"})
public void testMethod(){
System.out.println("testMethod, time: " + new Date());
}
}
3.6.3 使用Spring声明式异常处理没有权限的异常
ExceptionHandler
ControllerAdvice
3.7 从数据库中获取资源和权限
之前采用的方式:在spring-shiro.xml中配置
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
//...
<property name="filterChainDefinitions">
<value>
<!-- 允许匿名访问 -->
/login.jsp = anon
/shiro/login = anon
/shiro/logout = logout
/user.jsp = roles[user]
/admin.jsp = roles[admin]
# everything else requires authentication:
/** = authc
</value>
</property>
</bean>
</beans>
实际上是调用了ShiroFilterFactoryBean.class的setFilterChainDefinitions(String definitions)方法,而经过debug,发现setFilterChainDefinitionMap()方法传入的是LinkedHashMap,即将上述filterChainDefinitions中的values封装到Map中。
public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor {
public void setFilterChainDefinitions(String definitions) {
Ini ini = new Ini();
ini.load(definitions);
//did they explicitly state a 'urls' section? Not necessary, but just in case:
Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS);
if (CollectionUtils.isEmpty(section)) {
//no urls section. Since this _is_ a urls chain definition property, just assume the
//default section contains only the definitions:
section = ini.getSection(Ini.DEFAULT_SECTION_NAME);
}
setFilterChainDefinitionMap(section);
}
public void setFilterChainDefinitionMap(Map<String, String> filterChainDefinitionMap) {
this.filterChainDefinitionMap = filterChainDefinitionMap;
}
}
所以优化spring-shiro.xml:
<beana>
<!-- 3.配置 ShiroFilter -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
//...
<property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property>
</bean>
<!-- 配置一个 bean, 该 bean 实际上是一个 Map. 通过实例工厂方法的方式 -->
<bean id="filterChainDefinitionMap" factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"></bean>
<bean id="filterChainDefinitionMapBuilder"
class="com.cc.shiro.factory.FilterChainDefinitionMapBuilder"></bean>
</beans>
新建FilterChainDefinitionMapBuilder.java
public class FilterChainDefinitionMapBuilder {
public LinkedHashMap<String, String> buildFilterChainDefinitionMap(){
LinkedHashMap<String, String> map = new LinkedHashMap<>();
map.put("/login.jsp", "anon");
map.put("/shiro/login", "anon");
map.put("/shiro/logout", "logout");
//访问user.jsp,需要经过认证authc,并且还具有user权限
map.put("/user.jsp", "authc,roles[user]");
map.put("/admin.jsp", "authc,roles[admin]");
//如果开启rememberme,只需要有user角色就可以访问,无需认证
map.put("/success.jsp", "user");
map.put("/**", "authc");
return map;
}
}
3.8 会话管理
3.9 缓存管理
3.10 Remember Me
controller:设置token.setRememberMe(true);
@Controller
@RequestMapping("/shiro")
public class ShiroController {
@RequestMapping("/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password){
Subject currentUser = SecurityUtils.getSubject();
if (!currentUser.isAuthenticated()) {
// 把用户名和密码封装为 UsernamePasswordToken 对象
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// rememberme
token.setRememberMe(true);
//...
}
}
spring-shiro.xml:增加设置rememberme失效时间
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!-- 设置rememberme失效时间,单位s -->
<property name="rememberMeManager.cookie.maxAge" value="10"></property>
</bean>