文章目的
纯新手,记录一下从0开始学习使用shiro,并且结合手上的demo进行整个完整流程的知识点归纳总结。
目前已经实现的功能:整合shiro,完成基本配置,login
有什么不对希望多指教。
这是一个springboot 2.0 前后端分离项目
shiro 是干啥的?
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
核心组件
三个核心组件:Subject, SecurityManager 和 Realms.
Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。
Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。
工作流程
这是一个springboot项目,shiro这一块儿的主要代码来自
其他的业务代码来自我自己写的demo(这里面主要拿来测试功能了)
https://www.jianshu.com/p/7f724bec3dc3
先跟着无脑调通run起来,放进一个demo,跟着我的思路,进入哪里不懂点哪里的学习模式
Controller
@RestController
public class LoginController {
@RequestMapping("/login")
public String login(@RequestBody User user) {
//添加用户认证信息
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
user.getName(),
user.getPassword()
);
try {
//进行验证,这里可以捕获异常,然后返回对应信息
subject.login(usernamePasswordToken);
// subject.checkRole("admin");
// subject.checkPermissions("query", "add");
} catch (AuthenticationException e) {
e.printStackTrace();
return "账号或密码错误!";
} catch (AuthorizationException e) {
e.printStackTrace();
return "没有权限";
}
return "login success";
}
}
subject : subject 是 shiro 负责传递数据给 securityManager 的一个工具人。Subject代表着用户,用户所拥有的行为包括:登录、退出、校验权限、获得Session等
UsernamePasswordToken:之后从前台传递进来的用户信息将封装进这个对象里面,再通过subject的login方法来进行验证
subject.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安全管理器
在这个方法里面,重新制造了一个subject来接securityManager.login创建的subject
再进securityManager.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方法点进去一路找
最后
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
发现最后是一堆realm来获得这个信息
里面还有一堆对这个realm进行操作的源码,就不看了,知道realm是来处理这个当前登录的信息的,我们开始去处理realm
到这里shiro一家老小才拿到当前登录的用户信息。
拿到用户信息之后
拿到当前信息之后怎么操作其实就是由realm里面来设置
分两个部分:
1.写自己的realm
2.让shiro知道你要用这个写的realm
customrealm
先看代码
public class CustomRealm extends AuthorizingRealm {
@Resource
private LoginService loginService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取登录用户名
String name = (String) principalCollection.getPrimaryPrincipal();
//查询用户名称
User user = loginService.getUserByName(name);
//添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (Role role : user.getRoles()) {
//添加角色
simpleAuthorizationInfo.addRole(role.getRoleName());
//添加权限
for (Permissions permissions : role.getPermissions()) {
simpleAuthorizationInfo.addStringPermission(permissions.getPermissionsName());
}
}
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//加这一步的目的是在Post请求的时候会先进认证,然后在到请求
if (authenticationToken.getPrincipal() == null) {
return null;
}
//获取用户信息
String name = authenticationToken.getPrincipal().toString();
User user = loginService.getUserByName(name);
if (user == null) {
//这里返回后会报出对应异常
return null;
} else {
//这里验证authenticationToken和simpleAuthenticationInfo的信息
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, user.getPassword(), getName());
return simpleAuthenticationInfo;
}
}
}
已经有的信息:
自定义的realm继承自AuthorizingRealm
重写了doGetAuthorizationInfo和doGetAuthenticationInfo 两个方法
AuthorizingRealm
资料:Shiro 中的 Realm
结论:
1 自己写的realm一般都继承authorizingrealm就可以了
2 它的功能如下:实现了授权信息(角色、权限)的缓存、验证等功能。对授权信息(角色、权限)实现了缓存功能。
基本等于说只要继承了这个authorizingrealm就可以完成我们需要的所有关于权限的操作。
具体来讲就是把重写了doGetAuthorizationInfo和doGetAuthenticationInfo 两个方法给重写了就好了。
doGetAuthenticationInfo
功能:
负责鉴定登录的。既然是负责鉴定登录,那想一下需要做的几件事:
1.得到传进的数据
2.查数据库验证进来的数据对不对
3.查到了把验证的结果给出去(给出有的权限进入下一步)
过程:
首先是从传进来的authenticationToken里面拿到之前给进去的数据
先从数据库查验username来看看有没有这个user
没有的话直接返回null就会报异常(在controller里面catch)
有的话进入下一步去用shiro自己的办法查验密码(我目前密码没有加密存储在数据库里面,如果加密了的话是不是在这里对数据库里面的密码进行解密再传进去验证呢?有没有大哥提示一下。目前是什么情况暂时不知道,之后写到了再来修改)
封装成simpleAuthenticationInfo,给返回出去。
然后进入下一步来到了父类authenticatingRealm的getAuthenticationInfo方法
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//otherwise not cached, perform the lookup:
info = doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
cacheAuthenticationInfoIfPossible(token, info);
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
if (info != null) {
assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info;
可以看到先从cache里面找对应的信息,找不到开始就做比对验证
进入assertCredentialsMatch方法。是就是,不是就报异常。
doGetAuthorizationInfo:
授权的方法
多了一步去读取这个user里面的角色信息和权限信息。
具体流程因为暂时没有去测这部分留着之后写。
现在就是要让shiro知道用你的realm处理
写一个config,之前三大组件的最后一个SecurityManager出现
@Resource
CustomRealm customRealm;
//权限管理,配置主要是Realm的管理认证
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm);
return securityManager;
}
//Filter工厂,设置对应的过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> map = new HashMap<>();
//登出
map.put("/logout", "logout");
//对所有用户认证
map.put("/**", "authc");
//登录
shiroFilterFactoryBean.setLoginUrl("/login");
//首页
shiroFilterFactoryBean.setSuccessUrl("/index");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
先构建一个SecurityManager,这里主要是把自己写的Realm给set进去。源码看的脑子疼就先这样子设置把。
下面的是设置对应的filter链,根据其他的人的说明,是严格按照你写的先后顺序去执行的
意思如果你想指定某些请求不需要认证的话你必须写在对所有用户认证前面。
anon:不执行认证
authc:需要认证
public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor {
private static final transient Logger log = LoggerFactory.getLogger(ShiroFilterFactoryBean.class);
private SecurityManager securityManager;
private Map<String, Filter> filters = new LinkedHashMap();
private Map<String, String> filterChainDefinitionMap = new LinkedHashMap();
private String loginUrl;
private String successUrl;
private String unauthorizedUrl;
private AbstractShiroFilter instance;
这是里面的字段,只要看着设置就好了
这样子最后是可以完成登录/登陆后请求接口
这个基础功能的。之后我会继续完善把他写完再来更新
尚未摸清楚,但是实际开发场景可能会用到的一些东西
1.在鉴定登录信息的过程中是看到有remember me这个字段在传递的,但是尚不可知在哪里对这个功能的具体信息进行设置
2.在登录之后,可能会有相关的功能需要取到当前登录的用户信息(比如查看自己发布的东西或者其他),还不清楚在哪里可以取到(shiro好像是自带session的)
3.鉴权分角色限制资源访问,其实已经设置鉴权的realm。但是具体鉴权的流程还不太清楚,并且暂时没有去写。(这里面还有一块是怎么用注解来完成对各个接口的访问权限设定)
暂时没了
PEACE OUT