目录
- shiro 是什么?
- shiro 可以解决什么问题?
- shiro 名词解释&认证流程
- shiro 快速入门
- shiro ini Realm和JDBC Realm加入权限
- 自定义 shiro Realm
- 加盐和不加盐认证
1. shiro是什么?
Apache Shiro 是一个强大灵活的开源安全框架,提供认证、授权、会话管理以及密码加密等功能。
Apache Shiro 首要的目标是易于使用和理解。安全(相关的操作)有时很复杂很麻烦,不过这完全没必要,一个(安全)框架应该尽可能隐藏这其中的复杂性并提供一套简洁直观的 API 来简化开发人员在这方面的工作。
2. shiro可以解决什么问题?
使用 Apache Shiro 可以做到:
- 验证用户身份。
对用户进行访问控制,比如:- 判断某个用户是否被赋予某个特定角色
- 判断某个用户是否被允许执行某些操作
- 可以在各种环境下使用 Session API ,即使是不在web或EJB容器中。
- 对认证、访问控制或在会话生命周期中的事件进行响应处理。
- 可以聚合使用一个或多个安全数据的数据源而使用者只需了解一层抽象 。
- 使用单点登录(SSO)。
- 使用“下次自动登陆(Remember Me)”。
...
Shiro主要面向Shiro开发团队所谓的“应用安全的四大基础” ——认证,授权,会话管理与密码加密:
Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web 支持,可以非常容易的集成到 Web 环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
记住一点,Shiro 不会去维护用户、维护权限;这些需要我们自己去设计 / 提供;然后通过相应的接口注入给 Shiro 即可。
3. shiro 认证与授权流程分析
在概念层,Shiro 架构包含三个主要的理念:Subject,SecurityManager和 Realm。下面的图展示了这些组件如何相互作用,我们将在下面依次对其进行描述。
Subject:当前用户或主体,Subject 可以是一个用户,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它–当前和软件交互的任何事件。
SecurityManager:管理所有
Subject
,SecurityManager
是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。Realms:用于进行权限信息的验证,shiro 框架为我们提供默认的 Realm 我们可以根据需求自己实现。
Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)
3.1 用户认证
认证流程:
- 构建SecurityManager环境
- Subject(主体)提交认证
- SecurityManager 处理
- 流转到 Authenticator 执行认证
- 通过 Realm 获取相关的用户信息
3.2 用户授权
授权流程:
- 创建构建SecurityManager环境
- 主体提交授权认证
- SecurityManager 处理
- 流转到 Authorizor 授权器执行授权认证
- 通过 Realm 从数据库或配置文件获取角色权限数据返回给授权器,进行授权。
在 shiro 中,用户需要提供 principals (身份)和 credentials(证明)给 shiro,从而应用能验证用户身份:
principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个
principals
,但只有一个Primary principals
,一般是用户名、密码、手机号。credentials:证明 / 凭证,即只有主体知道的安全值,如密码、数字证书、token等。
最常见的 principals
和 credentials
组合就是用户名 / 密码了。接下来先进行一个基本的身份认证。
另外两个相关的概念是之前提到的 Subject
及 Realm
,分别是主体及验证主体的数据源。
4. shiro 快速入门
shiro 提供了默认的SimpleAccountRealm
域来进行用户认证和授权
4.1 shiro 用户认证
shiro 框架用户认证快速入门案例
/**
* shiro 用户认证
*/
@Test
public void authentication() {
//1.创建一个shiro默认SimpleAccountRealm 域
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
//2.构建SecurityManager环境
DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
//3.添加一个测试账号(后面可以做成读取动态读取数据库)
simpleAccountRealm.addAccount("zhangsan", "123456");
//4.设置Realm
defaultSecurityManager.setRealm(simpleAccountRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
//5.获取主体
Subject subject = SecurityUtils.getSubject();
//6.用户名和密码(用户输入的用户名密码)生成认证token
UsernamePasswordToken accessToken = new UsernamePasswordToken("zhangsan", "123456");
try {
//进行登入(提交认证)
subject.login(accessToken);
} catch (IncorrectCredentialsException exception) {
System.out.println("用户名密码不匹配");
} catch (LockedAccountException exception) {
System.out.println("账号已被锁定");
} catch (DisabledAccountException exception) {
System.out.println("账号已被禁用");
} catch (UnknownAccountException exception) {
System.out.println("用户不存在");
}
System.out.println("用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
//登出logout
System.out.println("执行 logout()方法后");
subject.logout();
System.out.println("用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
}
4.2 shiro 用户授权
/**
* shiro 用户授权
*/
@Test
public void authorization() {
//1.创建一个shiro默认SimpleAccountRealm 域
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
//2.构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//3.添加一个测试账号、和所拥有的角色(后面可以做成读取动态读取数据库)
simpleAccountRealm.addAccount("zhangsan", "123456", "admin", "user");
//4.设置Realm
defaultSecurityManager.setRealm(simpleAccountRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
//5.获取主体
Subject subject = SecurityUtils.getSubject();
//6.用户名和密码(用户输入的用户名密码)生成token
UsernamePasswordToken accessToken = new UsernamePasswordToken("zhangsan", "123456");
try {
//进行登入(提交认证)
subject.login(accessToken);
//检查权限
subject.checkRoles("admin", "user");
} catch (IncorrectCredentialsException exception) {
System.out.println("用户名密码不匹配");
} catch (LockedAccountException exception) {
System.out.println("账号已被锁定");
} catch (DisabledAccountException exception) {
System.out.println("账号已被禁用");
} catch (UnknownAccountException exception) {
System.out.println("用户不存在");
} catch (UnauthorizedException ae) {
System.out.println("用户没有权限");
}
System.out.println("用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
//登出logout
System.out.println("执行 logout()方法后");
subject.logout();
System.out.println("用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
}
5. shiro ini Realm和JDBC Realm加入权限
略过
6. 自定义 shiro Realm
Realm域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。
之前的案例都是利用 shiro 框架自带的 SimpleAccountRealm
域进行权限验证。现实开发中基本都是自定义 Realm 来进行验证
通常自定义Realm只需要继承:
AuthorizingRealm重写 doGetAuthenticationInfo
(用户认证)、doGetAuthorizationInfo
(用户授权) 这两个方法即可。
CustomRealm 自定义域
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CustomRealm extends AuthorizingRealm {
/**
* 模拟数据库用户“admin”、“test”
*/
private Map userMap =new HashMap<>();
{
userMap.put("admin","123456");
userMap.put("test","123456");
}
/**
* 模拟数据库通过用户名查询密码
* @param username
* @return
*/
private String getPasswordByUsername(String username) {
return userMap.get(username);
}
/**
* 模拟通过数据库获取用户角色信息
* @param username
* @return
*/
private List getRolesByUsername(String username) {
List roles = new ArrayList<>();
if(username.equals("admin")){
roles.add("admin");
}
roles.add("test");
return roles;
}
/**
* 模拟数据库通过用户名获得角色权限
* @param username
* @return
*/
private List getPerminssionsByUsername(String username) {
List permissions = new ArrayList<>();
if(username.equals("admin")){
permissions.add("user:delete");
permissions.add("user:add");
}
permissions.add("user:edit");
permissions.add("user:list");
return permissions;
}
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//String username1 = (String) SecurityUtils.getSubject().getPrincipal();
//获取用户名
String username = (String) principalCollection.getPrimaryPrincipal();
System.out.println("username:"+username);
//根据用户名获取“角色”与“权限”
List roles=getRolesByUsername(username);
List permissions=getPerminssionsByUsername(username);
//创建用户授权信息类
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//添加权限
authorizationInfo.addStringPermissions(permissions);
//添加角色
authorizationInfo.addRoles(roles);
//返回用户授权信息
return authorizationInfo;
}
/**
* 认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获取授权后的用户名
String username = (String) authenticationToken.getPrincipal();
//获取密码
String password=getPasswordByUsername(username);
if(StringUtils.isEmpty(password)){
return null;
}
//获取授权后的用户认证类并添加用户信息
SimpleAuthenticationInfo simpleAuthenticationInfo =new SimpleAuthenticationInfo(username,password,getName());
//返回用户认证
return simpleAuthenticationInfo;
}
}
测试类进行验证
/**
* 自定义Realm
*/
@Test
public void testCustomRealm(){
//1.获取自定义权限域
CustomRealm customRealm=new CustomRealm();
//2.构建SecurityManager环境
DefaultWebSecurityManager defaultWebSecurityManager=new DefaultWebSecurityManager();
//3.设置Realm
defaultWebSecurityManager.setRealm(customRealm);
SecurityUtils.setSecurityManager(defaultWebSecurityManager);
//4.获取主体
Subject subject = SecurityUtils.getSubject();
//5.获取登录用权限认证accessToken
UsernamePasswordToken accessToken = new UsernamePasswordToken("admin", "123456");
try {
subject.login(accessToken);
System.out.println("用户认证的状态" + subject.isAuthenticated());
subject.checkRoles("admin");
subject.checkPermissions("user:deleted","user:list","role:list");
subject.logout();
System.out.println("用户认证的状态" + subject.isAuthenticated());
//if no exception, that's it, we're done!
} catch (UnknownAccountException uae) {
System.out.println("用户不存在");
} catch (IncorrectCredentialsException ice) {
System.out.println("用户密码不匹配");
} catch (LockedAccountException lae) {
System.out.println("账号一杯锁定");
} catch (AuthenticationException ae) {
System.out.println("用户认证异常");
}catch (UnauthorizedException e){
System.out.println("该用户没有权限访问");
}
}
7. 不加盐和加盐认证
实际业务中用户的密码都是通过MD5+随机盐的方式进行存储到数据库中。shiro 框架也给我们提供方法供我们使用
7.1 不加盐认证
对 #6 的上述代码进行改造
doGetAuthenticationInfo
不加盐加密
/**
* 不加盐值获取密码
* @param password
* @return
*/
private String getEncPassword(String password){
return new Md5Hash(password,null,3).toString();
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获取授权后的用户名
String username = (String) authenticationToken.getPrincipal();
//获取密码
String password=getPasswordByUsername(username);
if(StringUtils.isEmpty(password)){
return null;
}
//不加盐值时获取明文密码用户信息认证
SimpleAuthenticationInfo simpleAuthenticationInfo =
new SimpleAuthenticationInfo(username,getEncPassword(password),getName());
//返回用户认证
return simpleAuthenticationInfo;
}
测试类进行测试
/**
* 不加盐认证
*/
@Test
public void testMatcher(){
DefaultWebSecurityManager defaultWebSecurityManager=new DefaultWebSecurityManager();
CustomRealm customRealm=new CustomRealm();
//创建加密类
HashedCredentialsMatcher matcher=new HashedCredentialsMatcher();
//加密方式
matcher.setHashAlgorithmName("md5");
//加密次数
matcher.setHashIterations(3);
customRealm.setCredentialsMatcher(matcher);
defaultWebSecurityManager.setRealm(customRealm);
SecurityUtils.setSecurityManager(defaultWebSecurityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("admin", "123456");
try {
subject.login(usernamePasswordToken);
System.out.println("用户认证的状态" + subject.isAuthenticated());
subject.checkRoles("test");
subject.checkPermissions("user:deleted","user:list","role:list");
subject.logout();
System.out.println("用户认证的状态" + subject.isAuthenticated());
//if no exception, that's it, we're done!
} catch (UnknownAccountException uae) {
System.out.println("用户不存在");
} catch (IncorrectCredentialsException ice) {
System.out.println("用户密码不匹配");
} catch (LockedAccountException lae) {
System.out.println("账号一杯锁定");
} catch (AuthenticationException ae) {
System.out.println("用户认证异常");
}catch (UnauthorizedException e){
System.out.println("该用户没有权限访问");
}
}
7.2 加盐认证
对 #6 的上述代码进行改造
/**
* 模拟数据库拿到加盐密码进行传值
* @param password
* @param salt
* @return
*/
private String getEncPassword(String password,String salt) {
return new Md5Hash(password, salt, 3).toString();
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获取授权后的用户名
String username = (String) authenticationToken.getPrincipal();
//获取密码
String password=getPasswordByUsername(username);
if(StringUtils.isEmpty(password)){
return null;
}
//加盐值获取密文密码用户信息认证
SimpleAuthenticationInfo simpleAuthenticationInfo =
new SimpleAuthenticationInfo(username,getEncPassword(password,username),getName());
simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(username));
//返回用户认证
return simpleAuthenticationInfo;
}
测试类进行测试
//加盐认证
@Test
public void testSaltMatcher(){
DefaultWebSecurityManager defaultWebSecurityManager=new DefaultWebSecurityManager();
CustomRealm customRealm=new CustomRealm();
HashedCredentialsMatcher matcher=new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("md5");
matcher.setHashIterations(3);
customRealm.setCredentialsMatcher(matcher);
defaultWebSecurityManager.setRealm(customRealm);
SecurityUtils.setSecurityManager(defaultWebSecurityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("admin", "123456");
try {
subject.login(usernamePasswordToken);
System.out.println("用户认证的状态" + subject.isAuthenticated());
subject.checkRoles("test");
subject.checkPermissions("user:deleted","user:list","role:list");
subject.logout();
System.out.println("用户认证的状态" + subject.isAuthenticated());
//if no exception, that's it, we're done!
} catch (UnknownAccountException uae) {
System.out.println("用户不存在");
} catch (IncorrectCredentialsException ice) {
System.out.println("用户密码不匹配");
} catch (LockedAccountException lae) {
System.out.println("账号一杯锁定");
} catch (AuthenticationException ae) {
System.out.println("用户认证异常");
}catch (UnauthorizedException e){
System.out.println("该用户没有权限访问");
}
}