什么是shiro?
shiro,安全框架。它的认证,授权,加密和会话管理可以用于保护任何应用程序。
shiro为以下几个方面提供应用程序的安全API(应用程序安全的4大基石):
Authentication - 提供用户身份认证,俗称登录
Authorization - 访问权限控制
Cryptography - 使用加密算法保护或者隐藏数据
Session Management - 用户的会话管理
Login
登录时需要进行shiro认证。以自己的项目为例,如下图:
在这个方法里会有shiro认证的过程。
shiro认证
这个过程主要分为三个部分:
- 1:获取客户端输入放入用户名,密码。
- 2:获取数据源中存放的数据即相应的用户名,密码。
- 3:进行两者的比对,判断是否登录操作成功。
来看看shiro的认证过程吧:
一句很重要的解释:通过当前的用户对象Subject执行login()方法将用户信息传给Shiro的SecurityManager,而这个SecurityManager会将用户信息委托给内部登录模块,由内部登录模块来调用Realm中的方法来进行数据比对进而判断是否登录成功。
这里有三个词语需要解释一下:
Subject:基于当前用户
SecurityManager:shiro架构核心,协调内部安全组件(如登录,授权,数据源等),用来管理所有的subject。
Realm:Realm充当的是Shiro和应用程序安全数据之间的==“桥梁”或“连接器”==。封装了数据库连接细节,将关联的数据提供给shiro。
获取Subject:
Subject subject = SecurityUtils.getSubject();
Login():
subject.login(token);
看一下login()的具体实现:
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
Subject subject = securityManager.login(this, token);
PrincipalCollection principals;
String host = null;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
//we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
String msg = "Principals returned from securityManager.login( token ) returned a null or " +
"empty value. This value must be non null and populated with one or more elements.";
throw new IllegalStateException(msg);
}
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false);
if (session != null) {
this.session = decorate(session);
} else {
this.session = null;
}
}
要注意的是内部调用的是securityManager.login(this, toke)方法。
我们再来进一步的看一下securityManager.login(this, toke)的内部实现:
首先SecurityManager中对login方法的声明:
Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;
实现类DefaultSecurityManager中对login()的实现:
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token);
} catch (AuthenticationException ae) {
try {
onFailedLogin(token, ae, subject);
} catch (Exception e) {
if (log.isInfoEnabled()) {
log.info("onFailedLogin method threw an " +
"exception. Logging and propagating original AuthenticationException.", e);
}
}
throw ae; //propagate
}
Subject loggedIn = createSubject(token, info, subject);
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
在这里我们发现调用了authenticate(token)
这个方法是从哪里来的呢?再来看看SecurityManager接口中的方法和它所继承的类:
public interface SecurityManager extends Authenticator, Authorizer, SessionManager {
Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;
void logout(Subject subject);
Subject createSubject(SubjectContext context);
}
这里我们看到 SecurityManager 接口继承了 Authenticator 登录认证的接口比如登录(Authenticator),权限验证(Authorizer)等。
再来看一看Authenticator接口中都声明了哪些方法:
public interface Authenticator {
public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)
throws AuthenticationException;
}
也就是我们刚才在DefaultSecurityManager中对login()的实现中调用的方法,忘了的小盆友可以回过头去看一眼哦,O(∩_∩)O哈哈~。
AbstractAuthenticator中authenticate()的实现:
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
// 调用doAuthenticate方法
info = doAuthenticate(token);
if (info == null) {
...
}
} catch (Throwable t) {
...
}
...
}
调用了doAuthenticate(token)方法。
我们再来看ModularRealmAuthenticator中doAuthenticate(token)方法的实现:
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
// Realm唯一时
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
调用了doSingleRealmAuthentication(realms.iterator().next(), authenticationToken),再往下看:doSingleRealmAuthentication的实现:
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
...
}
// 调用Realm的getAuthenticationInfo方法获取AuthenticationInfo信息
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
...
}
return info;
}
哇,我们看到了什么!!!!
realm.getAuthenticationInfo(token)
它调用Realm的getAuthenticationInfo(token)方法。
而在Realm中我们看一下用户认证方法重写:
package com.sh.demo.common.shiro;
import com.sh.demo.common.util.ShiroUtils;
import com.sh.demo.core.entity.SysMenuEntity;
import com.sh.demo.core.entity.SysRoleEntity;
import com.sh.demo.core.entity.SysUserEntity;
import com.sh.demo.core.service.SysMenuService;
import com.sh.demo.core.service.SysRoleService;
import com.sh.demo.core.service.SysUserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @Description Shiro权限匹配和账号密码匹配
* @Author Sans
* @CreateTime 2019/6/15 11:27
*/
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysRoleService sysRoleService;
@Autowired
private SysMenuService sysMenuService;
/**
* 授权权限
* 用户进行权限验证时候Shiro会去缓存中找,如果查不到数据,会执行这个方法去查权限,并放入缓存中
* @Author Sans
* @CreateTime 2019/6/12 11:44
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取用户ID
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
SysUserEntity sysUserEntity = (SysUserEntity) principalCollection.getPrimaryPrincipal();
Long userId =sysUserEntity.getUserId();
//这里可以进行授权和处理
Set<String> rolesSet = new HashSet<>();
Set<String> permsSet = new HashSet<>();
//查询角色和权限(这里根据业务自行查询)
List<SysRoleEntity> sysRoleEntityList = sysRoleService.selectSysRoleByUserId(userId);
for (SysRoleEntity sysRoleEntity:sysRoleEntityList) {
rolesSet.add(sysRoleEntity.getRoleName());
List<SysMenuEntity> sysMenuEntityList = sysMenuService.selectSysMenuByRoleId(sysRoleEntity.getRoleId());
for (SysMenuEntity sysMenuEntity :sysMenuEntityList) {
permsSet.add(sysMenuEntity.getPerms());
}
}
//将查到的权限和角色分别传入authorizationInfo中
authorizationInfo.setStringPermissions(permsSet);
authorizationInfo.setRoles(rolesSet);
return authorizationInfo;
}
/**
* 身份认证
* @Author Sans
* @CreateTime 2019/6/12 12:36
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获取用户的输入的账号.
String username = (String) authenticationToken.getPrincipal();
//通过username从数据库中查找 User对象,如果找到进行验证
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
SysUserEntity user = sysUserService.selectUserByName(username);
//判断账号是否存在
if (user == null) {
throw new AuthenticationException();
}
//判断账号是否被冻结
if (user.getState()==null||user.getState().equals("PROHIBIT")){
throw new LockedAccountException();
}
//进行验证
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user, //用户名
user.getPassword(), //密码
ByteSource.Util.bytes(user.getSalt()), //设置盐值
getName()
);
//验证成功开始踢人(清除缓存和Session)
ShiroUtils.deleteCache(username,true);
return authenticationInfo;
}
}
主要重写了俩个方法:
doGetAuthenticationInfo()主要是进行登录认证
doGetAuthorizationInfo()主要是进行角色权限和对应权限的添加
Shiro 配置:
要配置的是ShiroConfig类,Apache Shiro 核心通过 Filter 来实现(类似SpringMvc 通过DispachServlet 来主控制一样)
filter主要是通过URL规则来进行过滤和权限校验,所以我们需要定义一系列关于URL的规则和访问权限。如下:
@Configuration
public class ShiroConfig {
/**
* 身份验证器
* @Author Sans
* @CreateTime 2019/6/12 10:37
*/
@Bean
public ShiroRealm shiroRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return shiroRealm;
}
/**
* 安全管理器
* @Author Sans
* @CreateTime 2019/6/12 10:34
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 自定义Ssession管理
securityManager.setSessionManager(sessionManager());
// 自定义Cache实现
securityManager.setCacheManager(cacheManager());
// 自定义Realm验证
securityManager.setRealm(shiroRealm());
return securityManager;
}
/**
* Shiro基础配置
* @Author Sans
* @CreateTime 2019/6/12 8:42
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 注意过滤器配置顺序不能颠倒
// 配置过滤:不会被拦截的链接
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/userLogin/**", "anon");
filterChainDefinitionMap.put("/**", "authc");
// 配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
shiroFilterFactoryBean.setLoginUrl("/userLogin/unauth");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
}
方法一:ShiroRealm()方法
主要是将我自定义的匹配器对象当做参数传给ShiroRealm并返回。
(也就是说把我自定义来判断规则告诉shiro让shiro来管理)
而在我自定义的密码匹配器中是这样实现的:
/**
* 凭证匹配器
* 将密码校验交给Shiro的SimpleAuthenticationInfo进行处理,在这里做匹配配置
* @Author Sans
* @CreateTime 2019/6/12 10:48
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
// 散列算法:这里使用SHA256算法;
shaCredentialsMatcher.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME);
// 散列的次数,比如散列两次,相当于 md5(md5(""));
shaCredentialsMatcher.setHashIterations(SHA256Util.HASH_ITERATIONS);
return shaCredentialsMatcher;
}
这里我使用的是将客户端的密码进行加盐处理之后再和我数据库中的数据进行比对判断。
方法二:securityManager()
实例化了DefaultWebSecurityManager类,将上面ShiroRealm()的返回值当做参数,也就是配置Realm的管理认证。
方法三:shiroFilterFactory(DefaultWebSecurityManager securityManager)
Filter工厂,设置对应的过滤条件和跳转条件。
异常捕获
在登录过程中可能会出现不同的异常,对于不同的异常,我们是如何处理的呢?
当然不同的异常就要分类进行处理,比如密码错误和账户不存在就不能一概而论,对于这些问题,我们能做的就是将不同的异常进行捕获进行不同页面的跳转反馈给用户,提高用户体验,比如:
/**
* 登录
* @Author Sans
* @CreateTime 2019/6/20 9:21
*/
@RequestMapping("/login")
public Map<String,Object> login(@RequestBody SysUserEntity sysUserEntity){
Map<String,Object> map = new HashMap<>();
//进行身份验证
try{
//验证身份和登陆
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(sysUserEntity.getUsername(), sysUserEntity.getPassword());
//进行登录操作
subject.login(token);
}catch (IncorrectCredentialsException e) {
map.put("code",500);
map.put("msg","用户不存在或者密码错误");
return map;
} catch (LockedAccountException e) {
map.put("code",500);
map.put("msg","登录失败,该用户已被冻结");
return map;
} catch (AuthenticationException e) {
map.put("code",500);
map.put("msg","该用户不存在");
return map;
} catch (Exception e) {
map.put("code",500);
map.put("msg","未知异常");
return map;
}
map.put("code",0);
map.put("msg","登录成功");
map.put("token",ShiroUtils.getSession().getId().toString());
return map;
}