shiro安全框架
文章目录
shiro 是什么
Apache Shiro 是 Java 的一个安全(权限)框架。它可以非常容易的开发出足够安全的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境 。
Shiro 可以完成:认证、授权、加密、会话管理、与Web 集成、缓存 等。下载:http://shiro.apache.org/ 或 https://github.com/apache/shiro
功能介绍
Shiro目标:Shiro开发团队所称的“应用程序安全”的四个基石——身份验证、授权、会话管理和密码
- Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户 对某个资源是否具有某个权限;
- Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境,也可以是 Web 环境的;
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
还有额外的功能来支持和增强:
- Web Support:Web 支持,可以非常容易的集成到Web 环境;
- **Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
- Concurrency:Shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,就能把权限自动传播过去;
- Testing:提供测试支持,测试支持的存在是为了帮助您编写单元测试和集成测试,确保您的代码将是安全的。
- Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
- Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了
Shiro术语
- Subject:应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心就是 Subject。Subject 代表了当前“用户”
- Realm:Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/ 权限进行验证用户是否能进行操作。
Shiro架构
从外部来看Shiro ,即从应用程序角度的来观察如何使用 Shiro 完成工作:
- Subject(org.apache.shiro.subject.Subject):应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心就是 Subject。Subject 代表了当前“用户”, 这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫, 机器人等;与 Subject 的所有交互都会委托给 SecurityManager; Subject 其实是一个门面,SecurityManager 才是实际的执行者;
- SecurityManager (org.apache.shiro.mgt.SecurityManager):安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;其管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与 Shiro 的其他组件进行交互,它相当于 SpringMVC 中 DispatcherServlet 的角色
- Realm (org.apache.shiro.realm.Realm):Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/ 权限进行验证用户是否能进行操作;可以有一个或多个Realm来自定义,我们在自定义的Realm中获得数据库中真实的用户名和密码,来与传入的用户名和密码进行比较。
从Shiro内部来看:
- Authenticator(org.apache.shiro.authc.Authenticator):认证器,身份验证负责 Subject 认证,执行和对验证用户(登录),可以自定义实现;可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
- Authorizer(org.apache.shiro.authz.Authorizer):授权器、即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
- SessionManager (org.apache.shiro.session.mgt.SessionManager):管理 Session 生命周期的组件;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境
- SessionDAO:对 session 的 CURD 操作
- CacheManager(org.apache.shiro.cache.CacheManager):缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据 基本上很少改变,放到缓存中后可以提高访问的性能
- Cryptography (org.apache.shiro.crypto.*):密码模块,Shiro 提高了一些常见的加密组件用于如密码加密/解密
shiro 的主要功能 - 身份认证
1 Subject 认证
身份认证就是在应用中谁能证明他就是他本人,一般会使用用户名和密码作为认证信息。
2 Subject 认证主体
Subject 认证主体包含两个信息:
- Principals:身份,即用户名
- Credentials:凭证,即密码
3 认证流程
- 用户发送请求进行 Subject 认证(调用 subject.login(token))
- SecurityManager 会去 Authenticator(认证器)中查找相应的 Realms(可能不止一个)源
- Realms 可以根据不同类型的 Realm 中去查找用户信息,并进行判断是否认证成功
4 快速搭建 helloWorld
1.导包
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.12</version>
</dependency>
</dependencies>
2.创建 Realm /resources/shiro.ini
[users]
acey=123456
jack=111
3进行身份验证
public class HelloWorld {
public static void main(String[] args) {
// 加载配置文件,初始化 SecurityManager 工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory
("classpath:shiro.shiro.ini");
// 获取 SecurityManager 实例
SecurityManager securityManager = factory.getInstance();
// 把 SecurityManager 绑定到 SecurityUtils 中
SecurityUtils.setSecurityManager(securityManager);
// 得到当前执行的用户
Subject currentUser = SecurityUtils.getSubject();
//测试使用Session
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 + "]");
}
if (!currentUser.isAuthenticated()) {
//创建 token 令牌,用户名/密码
UsernamePasswordToken token = new UsernamePasswordToken("acey", "123456");
token.setRememberMe(true);
try {
//登录
currentUser.login(token);
System.out.println("登录成功");
} catch (UnknownAccountException uae) {
//没有指定的账户
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
//账户存在,密码不匹配
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) {
//用户被锁定异常
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... catch more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
//测试是否有某个角色
if (currentUser.hasRole("schwartz")) {
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}
//测试用户是否有某种权限
if (currentUser.isPermitted("lightsaber:wield")) {
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("winnebago:drive:eagle5")) {
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!");
}
//登出
currentUser.logout();
System.exit(0);
}
}
shiro 的主要功能 - 授权
权限授权就是访问控制,在应用中控制谁能访问哪些资源
1 权限认证中的几个元素
- 权限:即操作某个资源的权限,这些资源可以是某个链接,也可以是某个图片,也可以是对某个模块的数据的 CURL
- 角色:即权限的集合,一个角色可以有多个权限
- 用户:代表访问的用户,即 Subject
2 授权的流程
- 当用户访问应用中的某个资源时,会被 SecurityManager 拦截.
- SecurityManager 会去调用 Authorizer(授权器)
- Authorizer 会根据 Subject 的身份去相应的 Realm 中去查找该 Subject 是否有权限去访问该资源
3 授权实现
1 导包
2 配置 permission(权限) resources/shiro_permission.ini
[main]
authc.loginUrl=/login //表示用户登录失败跳转到 /login
roles.unauthorrizedUrl=/unauthorrized.jsp //表示用户没有对应的访问角色跳转到/unauthorrized.jsp
perms.unauthorrizedUrl=/unauthorrized.jsp //表示用户没有对应的访问权限跳转到/unauthorrized.jsp
[users]
acey=123456,role1,role2
jack=123,role1
[roles]
role1=user:select // role1 角色有访问 user:select 的权限
role2=user:add,/delete //role2 角色有访问 user:add 和 /delete 的权限
[urls]
/login=anon //表示任何用户都可以访问 /login
/index=authc //表示只有身份认证通过的用户才可以访问 /index
/index=roles[role1,role2...] //表示只有用户含有 role1 role2 ... 角色才可以访问 /index
/index=perms["user:create","/update"] //表示只有用户含有 "user:create"
和"/update"权限才可以访问 /index
/index?=authc //`?`通配符,表示一个字符,如/index1 /indexa /index- (不能匹配/index) ,
将符合这种规则的请求进行`authc`拦截
/index*=authc `*`通配符,表示零个或一个或多个字符,如/index1213asd /index /index2 ,
将符合这种规则的请求进行`authc`拦截
/index/**=authc `**`表示匹配零个或一个或多个路径,如/index/create /index/create/update/... ,
将符合这种规则的请求进行`authc`拦截
/index*/**authc 可以匹配 /index12/create/update/...
3 配置 roles (角色) resources/shiro_role.ini
[users]
acey=123456,role1,role2 //表示有一个用户,用户名是acey,密码为123456,有role1和role2角色
jack=123,role1
4 验证用户角色是否足够
public class RoleTest {
// 使用 checkRole 来检验角色时,若权限不足会返回 false
@Test
public void testHasRole() {
Subject currentUser= ShiroUtil.login("classpath:shiro_role.ini", "acey", "123456");
// Subject currentUser=ShiroUtil.login("classpath:shiro_role.ini", "jack", "123");
System.out.println(currentUser.hasRole("role1")?"has role1":"has not role1");
currentUser.logout();
}
// 使用 checkRole 来检验角色时,若权限不足会抛出异常
@Test
public void testCheckRole() {
Subject currentUser=ShiroUtil.login("classpath:shiro_role.ini", "acey", "123456");
// Subject currentUser=ShiroUtil.login("classpath:shiro_role.ini", "jack", "123");
currentUser.checkRole("role1");
currentUser.logout();
}
}
5 验证用户权限是否足够
public class PermissionTest {
// 使用 checkPermission 来检验权限时,若权限不足会返回 false
@Test
public void testIsPermitted() {
Subject currentUser= ShiroUtil.login("classpath:shiro_permission.ini", "acey", "123456");
System.out.println(currentUser.isPermitted("user:select")?"has user:select":"hsa not user:select");
currentUser.logout();
}
// 使用 checkPermission 来检验权限时,若权限不足会抛出异常
@Test
public void testCheckPermitted() {
Subject currentUser=ShiroUtil.login("classpath:shiro_permission.ini", "acey", "123456");
// Subject currentUser=ShiroUtil.login("classpath:shiro_permission.ini", "jack", "123");
currentUser.checkPermission("user:select");
currentUser.logout();
}
}
ssm 和 shiro 整合
导入依赖
2)配置 web.xml(shiro过滤器)
<!-- 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>
3)编写自己的 Realm(一般权限都是从数据库中查找,所以需要自定义)
public class MyRealm extends AuthorizingRealm{
@Resource
private UserService userService;
/**
* 为当前登录的用户授予角色和权限
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取用户名
String userName=(String)principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
//进行授权角色
authorizationInfo.setRoles(userService.getRoles(userName));
//进行授权权限
authorizationInfo.setStringPermissions(userService.getPermissions(userName));
return authorizationInfo;
}
/**
*验证当前登录的用户
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userName=(String)token.getPrincipal();
//根据用户名查找用户信息
User user=userService.getByUserName(userName);
if(user!=null){
AuthenticationInfo authcInfo=new SimpleAuthenticationInfo(user.getUserName(),user.getPassword(),getName());
return authcInfo;
}else{
return null;
}
}
}
4)spring 和 shiro 配置整合
<!-- 自定义Realm -->
<bean id="myRealm" class="com.acey.realm.MyRealm"/>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myRealm"/>
</bean>
<!-- Shiro过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- Shiro的核心安全接口,这个属性是必须的 -->
<property name="securityManager" ref="securityManager"/>
<!-- 身份认证失败,则跳转到登录页面的配置 -->
<property name="loginUrl" value="/index.jsp"/>
<!-- 权限认证失败,则跳转到指定页面 -->
<property name="unauthorizedUrl" value="/unauthor.jsp"/>
<!-- Shiro连接约束配置,即过滤链的定义 -->
<property name="filterChainDefinitions">
<value>
/login=anon
/admin*=authc
/student=roles[teacher]
/teacher=perms["user:create"]
</value>
</property>
</bean>
<!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- 开启Shiro注解 -->
<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>
一般角色和权限都存在数据库中,所以我们还可以自定义一个 filter 去自己验证每一个请求的 Subject 是否有权限去访问,这样我们就可以减少对过滤链的维护.比如
<!-- Shiro过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- Shiro的核心安全接口,这个属性是必须的 -->
<property name="securityManager" ref="securityManager"/>
<!-- 身份认证失败,则跳转到登录页面的配置 -->
<property name="loginUrl" value="/index.jsp"/>
<!-- 权限认证失败,则跳转到指定页面 -->
<property name="unauthorizedUrl" value="/unauthor.jsp"/>
<!-- Shiro连接约束配置,即过滤链的定义 -->
<property name="filterChainDefinitions">
<value>
/login=anon
/admin*=authc
/student=roles[teacher]
/teacher=perms["user:create"]
</value>
</property>
</bean>
可以改成
<!-- Shiro过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- Shiro的核心安全接口,这个属性是必须的 -->
<property name="securityManager" ref="securityManager"/>
<!-- 身份认证失败,则跳转到登录页面的配置 -->
<property name="loginUrl" value="/index.jsp"/>
<!-- 权限认证失败,则跳转到指定页面 -->
<property name="unauthorizedUrl" value="/unauthor.jsp"/>
<property name="ownFilter" class="ownFilter.class">
<!-- Shiro连接约束配置,即过滤链的定义 -->
<property name="filterChainDefinitions">
<value>
/login=anon
/**=ownFilter
</value>
</property>
</bean>
AuthenticationToken简介
AuthenticationToken 用于收集用户提交的身份(如用户名)及凭据(如密码)。
Shiro会调用 CredentialsMatcher 对象的 doCredentialsMatch 方法
对 AuthenticationInfo对象和 AuthenticationToken 进行匹配。
匹配成功则表示主体(Subject)认证成功,否则表示认证失败。
public interface AuthenticationToken extends Serializable {
Object getPrincipal(); //身份
Object getCredentials(); //凭据
}
public class UsernamePasswordToken implements HostAuthenticationToken, RememberMeAuthenticationToken {
//用于实现基于用户名/密码主体(Subject)身份认证。可以实现“记住我”及“主机验证”的支持。
private String username;
private char[] password;
private boolean rememberMe;
private String host;
...
}
public interface HostAuthenticationToken extends AuthenticationToken {
String getHost();// 获取用户“主机”
}
public interface RememberMeAuthenticationToken extends AuthenticationToken {
boolean isRememberMe();// 记住我
}
- 总结:
一般情况下UsernamePasswordToken已经可以满足我们的大我数需求。
当我们遇到需要声明自己的Token类时,
可以根据需求来实现AuthenticationToken,HostAuthenticationToken或RememberMeAuthenticationToken。
- 如果不需要“记住我”,也不需要“主机验证”,则可以实现AuthenticationToken;
- 如果需要“记住我”,则可以实现RememberMeAuthenticationToken;
- 如果需要“主机验证”功能,则可以实现HostAuthenticationToken;
- 如果需要“记住我”,且需要“主机验证”,则可以像UsernamePasswordToken一样,同时实现RememberMeAuthenticationToken和HostAuthenticationToken。
- 如果需要其他自定义功能,则需要自己实现。
SecurityUtils
- 在 Shiro 中 SecurityUtils 是一个抽象类。并且没有任何子类。在其中声明了一个静态属性,三个静态方法。
public abstract class SecurityUtils {
// 用来存储当前应用中全局唯一的一个SecurityManager。
private static SecurityManager securityManager;
public SecurityUtils() {
}
//Shiro 中最核心的方法了,用来获取 Subject.
public static Subject getSubject() {
Subject subject = ThreadContext.getSubject();
if (subject == null) {
subject = (new Builder()).buildSubject();
ThreadContext.bind(subject);
}
return subject;
}
public static void setSecurityManager(SecurityManager securityManager) {
securityManager = securityManager;
}
public static SecurityManager getSecurityManager() throws UnavailableSecurityManagerException {
SecurityManager securityManager = ThreadContext.getSecurityManager();
if (securityManager == null) {
securityManager = securityManager;
}
if (securityManager == null) {
String msg = "No SecurityManager accessible to the calling code, either bound to the " + ThreadContext.class.getName() + " or as a vm static singleton. This is an invalid application configuration.";
throw new UnavailableSecurityManagerException(msg);
} else {
return securityManager;
}
}
}
实现认证Realm
public class ShiroRealm extends AuthorizingRealm {
/**
* 身份认证 - 之后走下面的 授权
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1.把AuthenticationToken转换为UsernamePasswordToken
UsernamePasswordToken tokenInfo = (UsernamePasswordToken) authenticationToken;
// 2. 获取用户输入的账号
String telephone = tokenInfo.getUsername();
// 获取用户输入的密码
String password = String.valueOf(tokenInfo.getPassword());
// 通过telephone从数据库中查找 User对象,如果找到进行验证
// 这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
if (StringUtils.isEmpty(telephone)) {
// 用户账号未空的情况
return null;
}
//3从数据库获取 账号对应用户信息
SystemUserDO user = systemUserService.getUserByTelephone(telephone);
//4 判断账号是否存在
if (user == null) {
//返回null -> shiro就会知道这是用户不存在的异常
return null;
//throw new UnknownAccountException();
}
// 5验证密码 【注:这里不采用shiro自身密码验证 , 采用的话会导致用户登录密码错误时,已登录的账号也会自动下线!】
if (!MD5Util.checkPassword(password, user.getPassword())) {
throw new IncorrectCredentialsException("密码错误!");
}
// 6判断账号是否被冻结
if (user.getStatus() == null || user.getStatus().equals(1)) {
throw new LockedAccountException("用户账号已被冻结,禁止使用!");
}
//7 根据用户情况,构建AuthenticationInfo 对象并返回
//交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以在此判断或自定义实现
/**
* 进行验证 -> 注:shiro会自动验证密码
* 参数1:principal -> 放对象就可以在页面任意地方拿到该对象里面的值
* 参数2:hashedCredentials -> 密码
* 参数3:realmName -> 自定义的Realm
*/
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), getName());
// 如果用户已经存在登录信息,踢除用户
if (!StringUtils.isBlank(user.getToken())) {
ShiroUtils.deleteLoginInfo(user.getToken(), true);
}
// 验证成功开始踢人(清除缓存和Session)
// ShiroUtils.deleteCache(telephone, true);
// 认证成功后更新token
String token = ShiroUtils.getSession().getId().toString();
user.setToken(token);
user.setLastLoginDate(new Date());
user.setLastLoginDateStamp(user.getLastLoginDate().getTime());
user.setLastLoginIp(IPUtils.getRealIP(request));
// systemUserDAO.updateById(user);
systemUserService.save(user, true);
return authenticationInfo;
}
}
密码的MD5加密
方案一
自定义shiro的Realm实现和CredentialsMatcher实现以及Token实现 - 蓝萝卜blu - 博客园
方案二
自己封装一个MD5加密工具
自定义ShiroRealm extends AuthorizingRealm中身份认证时,将登录输入的密码和数据库中保存的密码进行比对,当然数据库中的密码是用户创建时就加密存入数据库的。
public class AuthenticationUtils {
/**
* 生成密码
*
* @param text 明文密码
* @return 加密过的密码
*/
public static String generatePassword(String text) {
return MD5Util.createMD5Str(text);
}
/**
* 检查密码是否一致
*
* @param external 外部输入的密码
* @param password 系统中记录的加密密码
* @return true|false
*/
public static boolean checkPassword(String external, String password) {
String externalPassword = generatePassword(external.toUpperCase());
if (externalPassword.equals(password.toUpperCase())) {
return true;
}
return false;
}
}
public class MD5Util {
/**
* 加密算法
**/
public final static String HASH_ALGORITHM_NAME = "MD5";
/**
* 循环次数
**/
public final static int HASH_ITERATIONS = 1;
public static String createMD5Str(String textPassword) {
/**
* 加密算法,原文,盐值,循环次数
*/
SimpleHash hash = new SimpleHash(
"MD5",
textPassword,
null,
HASH_ITERATIONS);
return hash.toString().toUpperCase();
}
}
//源码分析
class ShiroRealm extends AuthorizingRealm,class AuthorizingRealm extends 。AuthenticatingRealm中有个CredentialsMatcher比对器,是个接口,重写doCredentialsMatch方法来达到自定义比对方法
public interface CredentialsMatcher {
boolean doCredentialsMatch(AuthenticationToken var1, AuthenticationInfo var2);
}
public class SimpleCredentialsMatcher extends CodecSupport implements CredentialsMatcher {
...
}
最后分析源码得知,实现在HashedCredentialsMatcher类中,这里只贴出部分关键代码
public class HashedCredentialsMatcher extends SimpleCredentialsMatcher {
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenHashedCredentials = this.hashProvidedCredentials(token, info);
Object accountCredentials = this.getCredentials(info);
return this.equals(tokenHashedCredentials, accountCredentials);
}
protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) {
Object salt = null;
if (info instanceof SaltedAuthenticationInfo) {
salt = ((SaltedAuthenticationInfo)info).getCredentialsSalt();
} else if (this.isHashSalted()) {
salt = this.getSalt(token);
}
return this.hashProvidedCredentials(token.getCredentials(), salt, this.getHashIterations());
}
protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) {
String hashAlgorithmName = this.assertHashAlgorithmName();
return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
}
}
核心加密方法就是SimpleHash
public class SimpleHash extends AbstractHash {
/**
*algorithmName加密方法名
*source 原文
*salt 盐值
*hashIterations 循环计算次数
*/
public SimpleHash(String algorithmName, Object source, Object salt, int hashIterations) throws CodecException, UnknownAlgorithmException {
this.hexEncoded = null;
this.base64Encoded = null;
if (!StringUtils.hasText(algorithmName)) {
throw new NullPointerException("algorithmName argument cannot be null or empty.");
} else {
this.algorithmName = algorithmName;
this.iterations = Math.max(1, hashIterations);
ByteSource saltBytes = null;
if (salt != null) {
saltBytes = this.convertSaltToBytes(salt);
this.salt = saltBytes;
}
ByteSource sourceBytes = this.convertSourceToBytes(source);
this.hash(sourceBytes, saltBytes, hashIterations);
}
}
}
例如Md5加密
public class Md5Hash extends SimpleHash {
public static final String ALGORITHM_NAME = "MD5";
public Md5Hash() {
super("MD5");
}
public Md5Hash(Object source) {
super("MD5", source);
}
public Md5Hash(Object source, Object salt) {
super("MD5", source, salt);
}
public Md5Hash(Object source, Object salt, int hashIterations) {
super("MD5", source, salt, hashIterations);
}
public static Md5Hash fromHexString(String hex) {
Md5Hash hash = new Md5Hash();
hash.setBytes(Hex.decode(hex));
return hash;
}
public static Md5Hash fromBase64String(String base64) {
Md5Hash hash = new Md5Hash();
hash.setBytes(Base64.decode(base64));
return hash;
}
}
看到这里就可以直接自定义一个加密工具了
public class MD5Util {
/**
* 加密算法
**/
public final static String HASH_ALGORITHM_NAME = "MD5";
/**
* 循环次数
**/
public final static int HASH_ITERATIONS = 1;
/**
* 生成密码
*
* @param textPassword 明文密码
* @return 加密过的密码
*/
public static String createMD5Str(String textPassword) {
/**
* 加密算法,原文,盐值,循环次数
*/
SimpleHash hash = new SimpleHash(
"MD5",
textPassword,
null,
HASH_ITERATIONS);
return hash.toString().toUpperCase();
}
/**
* 检查密码是否一致
*
* @param external 外部输入的密码
* @param password 系统中记录的加密密码
* @return true|false
*/
public static boolean checkPassword(String external, String password) {
String externalPassword = createMD5Str(external.toUpperCase());
if (externalPassword.equals(password.toUpperCase())) {
return true;
}
return false;
}
}
有一个问题,如果两户账户密码一样,那加密得到的结果也一样,如何避免这种情况,提高密码的安全性呢?加点作料,加点盐。
认证策略 AuthenticationStrategy
/*
*
*/
package org.apache.shiro.authc.pam;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.realm.Realm;
import java.util.Collection;
/**
*
* @see AllSuccessfulStrategy
* @see AtLeastOneSuccessfulStrategy
* @see FirstSuccessfulStrategy
* @since 0.2
*/
public interface AuthenticationStrategy {
/**
*/
AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException;
/**
*
*/
AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException;
/**
*
*/
AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t)
throws AuthenticationException;
/**
*
*/
AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException;
}
AuthenticationStrategy 是个无状态的组件,在认证过程中会进行4次调用。
① 在所有Realm被调用之前
②在调用Realm的getAuthenticationInfo方法之前
③在调用Realm的getAuthenticationInfo 方法之后
④在所有Realm被调用之后
Shiro有3中认证策略的具体实现 AuthenticationStrategy类:
- AtLeastOneSuccessfulStrategy(默认)
只要一个或者多个Realm认证通过,则整体身份认证就会视为成功。 - FirstSuccessfulStrategy
只有第一个验证通过,才会视为整体认证通过。其他的会被忽略。 - AllSuccessfulStrategy
只有所有的Realm认证成功,才会被视为认证通过
自定义策略:继承org.apache.shiro.authc.pam.AbstractAuthenticationStrategy。
Realm顺序对认证是有影响的。
spring-shiro.xml 配置 认证策略:
<!-- Shiro默认会使用Servlet容器的Session,可通过sessionMode属性来指定使用Shiro原生Session -->
<!-- 这里主要是设置自定义的单Realm应用,若有多个Realm,可使用'realms'属性代替 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<!--<property name="realm" ref="myRealm"/>-->
<property name="authenticator" ref="authenticator"/>
</bean>
<!--多个realm 配置-->
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<!--配置认证策略-->
<property name="authenticationStrategy" ref="allSuccessfulStrategy"/>
<property name="realms">
<list>
<ref bean="firstRealm"/>
<ref bean="secondRealm"/>
</list>
</property>
</bean>
<!--全部通过-->
<bean id="allSuccessfulStrategy" class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"/>
<!--只有第一个验证通过,才会视为整体认证通过。其他的会被忽略。-->
<bean id="firstSuccessfulStrategy" class="org.apache.shiro.authc.pam.FirstSuccessfulStrategy"/>
<!--默认-->
<bean id="atLeastOneSuccessfulStrategy" class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"/>
授权
授权的概念
-
授权:也叫做访问控制,即在应用中控制谁能访问哪些资源(如访问页面、编辑数据、页面操作等)。在授权中需要了解几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)。
-
主体:即访问应用的用户,在Shiro中使用Subject代表用户。用户只有授权后才允许访问相应的资源
-
资源:在应用中用户可以访问的任何东西都称为资源。用户只有授权后才能访问。
-
权限:安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权利。即权限表示在应用中用户能不能访问某个资源。shiro支持粗颗粒度权限(如用户模块的所有权限)和细颗粒度权限(操作某个用户的权限,即实例级别的)。
-
角色:角色代表了操作集合,可以理解为权限的集合,一般情况下我们会赋予用户角色而不是权限,即这样用户可以拥有一组权限。不同的角色拥有一组不同的权限。
-
隐式角色:即直接通过角色来验证用户有没有操作权限。比如:在应用中班长和课代表可以使用打印机,但是某一天老师不允许课代表使用打印机了,我们就需要将应用中课代表操作打印机的权限代码删除。即粒度是以角色为单位进行访问控制的,粒度较粗。
-
显示角色:在程序中通过权限控制谁能访问某个资源,角色聚合一组权限集合;这样假设某个角色不能访问某个资源,只需要从角色代表的权限集合中移除即可;无需修改多处代码;即粒度是以资源、实例为单位的,粒度较细。
授权流程
解释:
- 首先调用Subject.isPermitted*/hasRole*接口,其会自动委托给SecurityManager,而SecurityManager会接着委托给Authorizer;
- Authorizer是真正的授权者,如果调用如isPermitted(“user:view”),其首先会通过PermissionResolver把真正的字符串转换成相应的Permission实例;
- 在进行授权之前,其会调用相应的Realm获取Subject相应的角色、权限用于匹配传入的角色、权限;
- Authorizer会判断Realm的角色、权限是否和传入的匹配,如果有多个Realm,或委托给ModularRealmAuthorizer进行循环判断,如果匹配如isPermitted*/hasRole*会返回true,否则返回false表示授权失败。
ModularRealmAuthorizer进行多Realm匹配流程:
- 首先检查相应的Realm是否实现了Authorizer;
- 如果实现了Authorizer,那么接着调用其相应的isPermitted*/hasRole*接口进行匹配;
- 如果有一个Realm匹配那么僵返回true,否则返回false;
如果Realm进行首选的话,应该继承AuthorizingRealm,其流程是:
- 如果调用hasRole*,则直接获取AuthorizationInfo.getRoles()与传入的角色比较即可
- 如果调用如isPermitted(“user:view”),首先通过PermissionResolver将权限字符串转换成相应的Permission实例,默认使用WildcardPermissionResolver,即转换为通配符的WildcardPermission;
- 通过AuthorizationInfo.getObjectPermissions()得到Permission实例集合;通过AuthorizationInfo.getStringPermissions()得到字符串集合并通过PermissionResolver解析为Permission实例;然后获取用户的角色,并通过RolePermissionResolver解析角色对应的权限集合(默认没有实现,可以自己提供);
- 接着滴啊用Permission.implies(Permission p)逐个与传入的权限比较,如果有匹配的则返回true,否则返回false.
授权方式
Shiro支持三种方式的授权:
编程式: 通过写if/else授权代码块完成:
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole("admin")){
//有权限
} else{
//无权限
}
注解式: 通过在指定的Java方法上放置相应的注解完成:
@RequiresRoles("admin")
public void hello(){
//有权限
}
没有权限将抛出相应的异常。
JSP标签: 在JSP页面通过相应的标签完成:
<shiro:hasRole name="admin">
<!-- 有权限 -->
</shiro:hasRole>
实现授权
基于角色
基于角色的访问控制(隐式角色)
1、shiro-role.ini
[users]
tycoding=123,role1,role2
补充
此处ini配置文件的规则:用户名=密码,角色1,角色2,…。如果在需要在应用中判断用户是否拥有相应角色,就需要需要在相应的Realm中返回角色信息,也就是说Shiro不负责维护用户-角色信息,Shiro只是提供了相应的接口方便验证。
2、RoleTest.java
@Test
public void testHasRole() {
//1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager
Factory factory = new IniSecurityManagerFactory("classpath:shiro-role.ini");
//2、得到SecurityManager实例,并绑定给SecurityUtils
SecurityManager securityManager = (SecurityManager) factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
//3、得到Subject及创建用户名、密码身份的Token
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("tycoding", "123");
try {
//4、登录,即身份验证
subject.login(token);
//判断用户是否拥有角色:role1
System.out.println(subject.hasRole("role1"));
//判断用户是否拥有角色:role1、role2
boolean[] check1 = subject.hasRoles(Arrays.asList("role1", "role2"));
for (boolean b: check1) {
System.out.println(b);
}
//判断用户是否拥有角色:role1、role2、role3
boolean[] check2 = subject.hasRoles(Arrays.asList("role1", "role2", "role3"));
for (boolean b: check2) {
System.out.println(b);
}
} catch (AuthenticationException e) {
//5、身份验证失败
e.printStackTrace();
}
//6、退出
subject.logout();
}
打印:
true
true
true
true
false
4、总结
如上就是基于角色的访问控制(即隐式角色),这种方式的缺点如果很多地方都进行了角色的判断,但是某一天不需要了,就要把相关的代码删除掉;这就是粗颗粒度造成的问题。
基于资源
基于资源的访问控制(显示角色)
1、shiro-permission.ini
[users]
tycoding=123,role1,role2
[roles]
role1:user:create,user:update
role2:user:create,user:delete
补充
此处ini配置文件的规则:”用户名=密码,角色1,角色2” “角色=权限1,权限2”。即首先根据用户名找到角色,然后再根据角色找到权限;即角色是权限的集合;Shiro同样不进行权限的维护,需要我们通过Realm返回相应的权限信息。只需要维护”用户-角色”之间的关系即可。
2、RoleTest.java
@Test
public void testPermissionRole() {
//1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager
Factory factory = new IniSecurityManagerFactory("classpath:shiro-permission.ini");
//2、得到SecurityManager实例,并绑定给SecurityUtils
SecurityManager securityManager = (SecurityManager) factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
//3、得到Subject及创建用户名、密码身份的Token
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("tycoding", "123");
try {
//4、登录,即身份验证
subject.login(token);
//判断用户是否拥有权限:user:create
System.out.println(subject.isPermitted("user:create"));
//潘墩用户是否拥有权限:user:update和user:delete
boolean[] check = subject.isPermitted("user:create", "user:delete");
for (boolean b: check) {
System.out.println(b);
}
} catch (AuthenticationException e) {
//5、身份验证失败
e.printStackTrace();
}
//6、退出
subject.logout();
}
3、打印结果:
true
true
true
4、总结
如上,我们事先了基于资源的访问控制(显示角色)。这种方式的优势显而易见,主要体现角色是权限的集合,这种方式的规则主要是资源标识符:操作,即是资源级别的粒度。如果我们需要更改某个角色的权限,只需要一个资源级别的修改,不会对其他模块代码产生影响,粒度小。需要维护用户–角色,角色–权限之间的关系。
Realm
- 为什么要重写realm?
因为在web开发中,我们需要realm内的真实数据是我们自己查出来的数据库中的数据,包含我们自己的逻辑,所以我们需要重写realm以存放我们的数据 - 如何去重写realm?
//Realm 继承体系
Realm
CachingRealm //支持数据缓存
AuthenticatingRealm //支持缓存/认证
AuthorizingRealm //支持缓存/认证/授权
ShiroFilterFactoryBean
- Shiro提供了与Web集成的支持,其通过一个ShiroFilter入口来拦截需要安全控制的URL,然后进行相应的控制
AccessControlFilter
- 是shiro-web模块当中比较重要的类,所有的拦截器都继承此类
- shiro-web 提供的filter,每种filter都对应了不同的权限拦截规则
shiro枚举过滤器
public enum DefaultFilter {
//匿名拦截器,即不需要登录即可访问,一般用于静态资源
anon(AnonymousFilter.class),
//基于表单的拦截器,没登录就跳转到指定登录页面
authc(FormAuthenticationFilter.class),
//Http身份验证拦截器,
authcBasic(BasicHttpAuthenticationFilter.class),
authcBearer(BearerHttpAuthenticationFilter.class),
ip(IpFilter.class),
//退出拦截器
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
//权限授权拦截器,验证用户是否拥有所有权限
perms(PermissionsAuthorizationFilter.class),
//端口拦截器,
port(PortFilter.class),
//rest风格拦截器,自动根据请求方法构建权限字符串
rest(HttpMethodPermissionFilter.class),
//角色授权,验证用户是否拥有角色
roles(RolesAuthorizationFilter.class),
//SSL拦截器,只有请求协议是https才能通过,否则会自动跳转https端口
ssl(SslFilter.class),
//用户拦截器,用户已经身份验证/记住我登录的都可
user(UserFilter.class),
invalidRequest(InvalidRequestFilter.class);
}
- AccessControlFilter继承至 ServletContextSupport
/**
* 开启Shiro-aop注解支持:使用代理方式所以需要开启代码支持
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
Shiro 标签
权限注解:
@RequiresAuthentication
表示当前Subject已经通过login进行了身份验证;即Subject. isAuthenticated()返回true。
@RequiresUser
表示当前Subject已经身份验证或者通过记住我登录的。
@RequiresGuest
表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。
@RequiresRoles(value={“admin”, “user”}, logical= Logical.AND)
表示当前Subject需要角色admin和user。
@RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR)
表示当前Subject需要权限user:a或user:b。
Shiro 会话管理
也可以用redis做session缓存
参考:
https://www.w3cschool.cn/shiro/co4m1if2.html
https://blog.csdn.net/shitianhang123/article/details/84942117