🍅一、shiro
🍑1.介绍
shiro
的官网对shiro
的介绍如下:apache shiro
是一个强大且使用简单的框架,可以用于身份验证、授权、加密、session
管理。可以被用来保证任何应用程序的安全,无论是web
程序还是手机应用程序。
apache shiro
提供了应用程序API
,来实现以下四个基础功能:
①Authentication
:提供用户认证,也就是我们说的登录账号和密码的校验。
②Authorization
:访问控制,也就是权限控制。
③Cryptography
:加密,保护和隐藏数据。
④Session Management
:session
管理。
除了这四个基础的功能之外,shiro
也提供一些其它的像单元测试,多线程支持,这些功能都是加强上面这四个基础功能。
🍒2.概念
shiro
包含了如下几个概念:
🥇Subject
:当前用户,当前用户也不仅仅是登录用户,也可以是其它应用。
🥈SecurityMananger
:核心管理器,管理Subject
和各个模块的交互
🥉Realms
:连接shiro
和数据源的桥梁,找到数据源进行验证,授权等操作。
🍓3.Realm
首先要定义一个Realm
用于连接数据源,shiro
帮助我们提供用户认证,相当于对输入的用户进行验证,那么我们就得找到存用户账号信息的数据源,把输入的用户和数据源里面的用户进行匹配。Realm
就是来连接数据源的。
Realm
是一个接口,有很多实现类,我们也可以自己通过继承来实现Realm
。这里我们介绍两个Realm
,下面用这两个Realm
示例来展示shiro
使用流程。
SimpleAccountRealm
:可以将账号信息存储在SimpleAccount
对象里面,然后放到SimpleAccountRealm
的一个Set
集合里面。
IniRealm
:将账号信息提前放在ini
文件中,IniRealm
可以直接从ini
文件中获取数据源。
🥝4.SecurityManager
SecurityManager
是一个接口,有很多的实现类,分别负责管理shiro
的认证、授权、session
管理、缓存管理等。SecurityManager
接口只有基础的登录、退出、创建Subject
方法
🥥5.Subject
Subject
接口对Subject
描述是表示用户的状态和安全操作,这些操作包含了认证,授权,session
访问。
这个描述有点抽象,简单点说就是Subject
表示当前用户,用户获取权限、判断权限、登录、退出、获取session
都是通过Subject
的方法来实现的。
除此之外还有一个WebSubject
接口,它继承了Subject
,多了获取ServletRequest
和ServletResponse
的方法。
🍇二、shrio使用
🍌1.SimpleAccountRealm
SimpleAccountRealm
类有一个SimpleAccount
集合,用于存储账号和密码
new
一个SecurityManager
把SimpleAccountRealm
放到SecurityManager
,然后再把SecurityManager
放到SecurityUtils
里面
再通过SecurityUtils
获取Subject
调用Subject
的login
方法去校验用户名密码
public class Demo {
@Test
public void testShiro() {
// 1.首先设置一个数据源,用一个最简单的Realm可以直接设置数据源
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
simpleAccountRealm.addAccount("zhangsan", "123");
// 2.构建一个核心SecurityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(simpleAccountRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
// 3.获取用户主体Subject
Subject Subject = SecurityUtils.getSubject();
System.out.println(Subject.isAuthenticated());
// 4.提交账号登录,实际运用过程中,这个应该是从前端传过来的
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("zhangsan", "123");
Subject.login(usernamePasswordToken);
System.out.println(Subject.isAuthenticated());
}
}
🍍2.IniRealm
上面我们用SimpleAccountRealm
来保存已有的账号密码,但是这样每次都要改代码,我们也可以通过ini
文件来提前录入一些账号密码,在resources
下面新建一个文件shiroAccount.ini
文件,文件名可以随意取,下面两个就是初始化的用户名,前面是用户名,等号后面是密码。
2.1 定义账号
在resources
下面新建一个shiroAccount.ini
文件,按照下面的格式添加账号和密码。
[users]
zhangsan=123
xiaoluo=456
2.2 使用
使用流程和SimpleAccountRealm
是一样的,只不过数据源是从ini
文件中获取的。
public class Demo {
@Test
public void testShiro() {
// 1.从ini文件中初始化数据源
IniRealm iniRealm = new IniRealm("classpath:shiroAccount.ini");
// 2.初始化核心SecurityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(iniRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
// 3.获取用户主体Subject
Subject Subject = SecurityUtils.getSubject();
System.out.println(Subject.isAuthenticated());
// 4.提交用户名和密码进行认证
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("xiaoluo", "456");
Subject.login(usernamePasswordToken);
System.out.println(Subject.isAuthenticated());
}
}
🍎3.自定义Realm
上面我们用了两个例子来展示shiro
怎么用的,但是实际项目开发中已经不这么使用了,现在我们的账号密码都保存在数据库中了,shiro
支持自定义Realm
,自定义Realm
可以通过实现Realm
接口,默认要实现两个方法:负责用户角色权限的doGetAuthorizationInfo
和负责用户名密码校验的doGetAuthenticationInfo
。
3.1 自定义Realm
自定义Realm
不仅要负责连接数据源,还要负责用户名和密码逻辑校验的编写。
public class MyRealm extends AuthorizingRealm {
/**
* 模拟数据库数据
*/
Map userMap = new HashMap(16);
{
userMap.put("zhangsan", "123");
userMap.put("lisi", "456");
}
/**
* 判断角色权限的
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return new SimpleAuthorizationInfo();
}
/**
* 判断用户名密码的
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String) authenticationToken.getPrincipal();
String password = getPasswordByUsername(username);
if (StringUtils.isEmpty(password)) {
return null;
}
// super.getName(),也可以自己设置个名字
return new SimpleAuthenticationInfo(username, password, super.getName());
}
private String getPasswordByUsername(String username) {
return userMap.get(username);
}
}
3.2 使用
public class Demo {
@Test
public void testShiro() {
MyRealm myRealm = new MyRealm();
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(myRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject Subject = SecurityUtils.getSubject();
System.out.println(Subject.isAuthenticated());
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("lisi", "456");
Subject.login(usernamePasswordToken);
System.out.println(Subject.isAuthenticated());
}
}
🍏4.使用总结
① 定义一个Realm
,Realm
的作用就是连接数据源,后续校验用户输入的账号和密码就是和Realm
连接的数据源去 做对比。
②定义一个SecurityManager
,这个是核心用来管理Subject
和各模块交互,把Realm
set
到SecurityManager
里面。
③把SecurityManager
,再放到SecurityUtils
里面去。
④通过SecurityUtils
获取Subject
,也就是当前用户。
⑤把当前用户账号密码封装成UsernamePasswordToken
。
⑥把UsernamePasswordToken
作为参数调用Subject
的login
方法去校验账号密码正确性,具体怎么校验就在Realm
里面。
🍈三、Springboot+realm
🍉1. 文件结构
主页index.html
,如果访问主页没有登录就会自动跳转到login.htm
页面。
用于登录的login.html
页面。
Realm
文件定义数据源。
查询数据库的UserService
。
处理登录的LoginController
文件。
shiroConfig
配置文件。
这里还需要引入shiro
的jar
包
org.apache.shiro
shiro-spring
1.5.3
🍊2. 代码示例
2.1 index.html
Title
登录成功,欢迎来到主页。
2.2 Login.html
登录
2.3 Realm
定义一个Realm
,这里的数据源我们从数据库获取账号密码进行校验,授权的逻辑这里就不关注,只关注账号密码校验的逻辑。
@Slf4j
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 判断角色权限的
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("执行授权逻辑");
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
return simpleAuthorizationInfo;
}
/**
* 判断用户名和密码的
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("开始判断用户名和密码");
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
// 这里通过service调用mapper层去数据库查询
User user = userService.selectUserByUsername(usernamePasswordToken.getUsername());
if (ObjectUtils.isEmpty(user)) {
return null;
}
return new SimpleAuthenticationInfo(user, user.getPassword(), "");
}
}
2.4 UserService
UserService
用于查询数据,这里模拟从数据库查询。
@Service
public class UserService {
private List userList;
{
userList = new ArrayList();
userList.add(new User("1", "zhangsan", "123"));
userList.add(new User("2", "lisi", "456"));
}
// 通过用户名去查询用户信息
public User selectUserByUsername (String username) {
for (User user : userList) {
if (StringUtils.equals(username, user.getUsername())) {
return user;
}
}
return null;
}
}
2.5 ShiroConfig
ShiroConfig
主要是配置shiro
的过滤器,主要用于拦截请求,判断用户是否登录。同时还要负责初始化SecurityManager
和Realm
对象。
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map filermap = new LinkedHashMap();
// 无需登录就可以访问
filermap.put("/study/login", "anon");
// 必须要登录才能访问
filermap.put("/study/*", "authc");
// 没有权限跳转的地址
shiroFilterFactoryBean.setUnauthorizedUrl("/Unauthorized");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filermap);
return shiroFilterFactoryBean;
}
//创建DefaultWebSecurityManager
@Bean(name="securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
return securityManager;
}
//创建Realm
@Bean(name = "userRealm")
public UserRealm getUserRealm(){
return new UserRealm();
}
}
2.6 LoginController
在userLogin
方法中负责把用户提交的用户名和密码提交到Realm
中进行认证,同时这里要对AuthenticationException
进行捕获,因为当账号密码校验失败之后会抛出AuthenticationException
异常。
@RestController
@RequestMapping
public class LoginController {
@PostMapping("/login")
public void userLogin (HttpServletRequest request, HttpServletResponse response) throws IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
try{
subject.login(usernamePasswordToken);
boolean authenticationResult = subject.isAuthenticated();
if (authenticationResult) {
response.sendRedirect("index.html");
} else {
response.sendRedirect("login.html");
}
} catch (AuthenticationException e) {
response.sendRedirect("login.html");
}
}
}
🍋3. 访问
首先访问index.html
页面,这个页面没有登录会跳转到login.html
页面
localhost:9090/study/index.html
登录之后会跳转到index.html
页面
如果登录失败会跳转到login.html
页面
🥭四、源码解读
🍅1. shiro拦截流程
1.1 OncePerRequestFilter
OncePerRequestFilter
实现了Filter
,也就被加入了过滤器链路中,每次的请求都会经过这个方法,我们了解shiro
的源码拦截流程也就从OncePerRequestFilter
这个类开始。
在OncePerRequestFilter
的doFilter
方法中调用了AbstractShiroFilter
类的doFilterInternal
方法。
1.2 AbstractShiroFilter
AbstractShiroFilter
类的doFilterInternal
方法做了三件事
😀三件事情:
🥇把HttpServletRequest
封装成ShiroHttpServletRequest
;把HttpServletResponse
封装成ShiroHttpServletResponse
。
🥈创建一个Subject
对象。
🥉把Subject
对象绑定到当前线程上面。
😃细说三件事:
上面的三件事,如果详细说的话其实只有创建Subject
是核心,这里我们只看创建Subject
的方法逻辑。
这里我们重点看createSubject
,这里创建Subject
是通过new
一个WebSubject
接口的内部类Builder
,在Builder
里面会创建SubjectContext
用于保存在整个过程中的所需的变量,这里先放所需的SecurityManager
、request
、response
。
然后调用buildWebSubject
方法来创建一个Subject
对象。
protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
}
1.3 WebSubject
WebSubject
的buildWebSubject
方法逻辑很简单:
- 调用了父类
Subject
的buildSubject
创建了一个Subject
对象 - 返回
Subject
对象
public WebSubject buildWebSubject() {
Subject subject = super.buildSubject();
if (!(subject instanceof WebSubject)) {
String msg = "Subject implementation returned from the SecurityManager was not a " +
WebSubject.class.getName() + " implementation. Please ensure a Web-enabled SecurityManager " +
"has been configured and made available to this builder.";
throw new IllegalStateException(msg);
}
return (WebSubject) subject;
}
1.4 Subject
在父类Subject
里面的buildSubject
也只是干了一件简单的事情,调用SecurityManager
的createSubject
方法。
public Subject buildSubject() {
return this.securityManager.createSubject(this.subjectContext);
}
这里就是核心代码了,创建session,保存session都是在这里做的。先看下源码
1.5 DefaultSecurityManager
public Subject createSubject(SubjectContext subjectContext) {
SubjectContext context = copy(subjectContext);
context = ensureSecurityManager(context);
context = resolveSession(context);
context = resolvePrincipals(context);
Subject subject = doCreateSubject(context);
save(subject);
return subject;
}
从源码中可以看出创建Subject
一共做了六件事:
🌷1.新建一个DefaultSubjectContext
,把前面的SubjectContext
里面的内容放到这里面来
🌸2.确保SubjectContext
里面有SecurityManager
🌹3.解析session
,并将session
放到SubjectContext
🌺4.解析用户,这里的用户指的是登录中的使用的用户对象
🌻5.创建一个Subject
🪷6.保存Subject
整个代码的调用链路到这里就完成了,下面我们就以这六件事为维度
1.6 六件事之copy
copy
就干了一件事把前面SubjectContext
里面的内容放到新创建的DefaultWebSubjectContext
,因为SubjectContext
继承了Map
类,所以这里的copy
就是调用了Map
的putAll
方法。
这里调用的是DefaultWebSecurityManager的copy方法
1.7 六件事之确保SecurityManager
这里的确保是确保新的这个DefaultWebSubject
里面有SecurityManager
,没有就把当前的SecurityManager
放到DefaultSubjectContext
。
1.8 六件事之解析Session
解析session
调用的是DefaultSecurityManager
类的resolveSession
方法,整个获取session
的调用链路如下
- DefaultSecurityManager-resolveSession
protected SubjectContext resolveSession(SubjectContext context) {
// 先从context里面解析是否有session,如果有就直接返回了
if (context.resolveSession() != null) {
log.debug("Context already contains a session. Returning.");
return context;
}
try {
// 如果没有的话就调用resolveContextSession去解析session
Session session = resolveContextSession(context);
// 如果session不为空就把session放到context里面去
if (session != null) {
context.setSession(session);
}
} catch (InvalidSessionException e) {
log.debug("Resolved SubjectContext context session is invalid. Ignoring and creating an anonymous (session-less) Subject instance.", e);
}
return context;
}
- DefaultSecurityManager-resolveContextSession
protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {
// 先获取SessionKey,这里的SessionKey可以看作sessionid
SessionKey key = getSessionKey(context);
// 然后再通过SessionKey来获取session
if (key != null) {
return getSession(key);
}
return null;
}
- ServletContainerSessionManager-getSession
public Session getSession(SessionKey key) throws SessionException {
if (!WebUtils.isHttp(key)) {
String msg = "SessionKey must be an HTTP compatible implementation.";
throw new IllegalArgumentException(msg);
}
HttpServletRequest request = WebUtils.getHttpRequest(key);
Session session = null;
// 通过request来获取session,参数为false,也就是没有也不创建
HttpSession httpSession = request.getSession(false);
if (httpSession != null) {
// 这里只是把HttpSession封装成HttpServletSession对象
session = createSession(httpSession, request.getRemoteHost());
}
return session;
}
- Request-getSession
public HttpSession getSession(boolean create) {
// 获取session
Session session = doGetSession(create);
// 如果session为空就返回空
if (session == null) {
return null;
}
// 如果不为空就返回HttpSession
return session.getSession();
}
- Request-doGetSession
protected Session doGetSession(boolean create) {
// 先获取Context,如果Context都为空,那就直接返回空了
Context context = getContext();
if (context == null) {
return null;
}
// 如果当前session存在但是无效的,也就是session过期了,通过过期时间判断
if ((session != null) && !session.isValid()) {
session = null;
}
// 如果session存在也没过期,那就直接返回
if (session != null) {
return session;
}
// 先获取Manager
Manager manager = context.getManager();
// Manager为空也返回
if (manager == null) {
return null;
}
// 这里就是前台页面传过来的sessionid了,通过这个ID去获取是不是存在session,当然这里还没有登陆,
// 这里 requestedSessionId肯定是为空的
if (requestedSessionId != null) {
try {
session = manager.findSession(requestedSessionId);
} catch (IOException e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("request.session.failed", requestedSessionId, e.getMessage()), e);
} else {
log.info(sm.getString("request.session.failed", requestedSessionId, e.getMessage()));
}
session = null;
}
// session不为空,但是失效了也返回空
if ((session != null) && !session.isValid()) {
session = null;
}
if (session != null) {
session.access();
return session;
}
}
// 这个create就是传进来的参数了,如果是false就是不创建,直接返回空
if (!create) {
return null;
}
boolean trackModesIncludesCookie =
context.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.COOKIE);
if (trackModesIncludesCookie && response.getResponse().isCommitted()) {
throw new IllegalStateException(sm.getString("coyoteRequest.sessionCreateCommitted"));
}
// 这里是获取SessionID
String sessionId = getRequestedSessionId();
if (requestedSessionSSL) {
} else if (("/".equals(context.getSessionCookiePath())
&& isRequestedSessionIdFromCookie())) {
if (context.getValidateClientProvidedNewSessionId()) {
boolean found = false;
for (Container container : getHost().findChildren()) {
Manager m = ((Context) container).getManager();
if (m != null) {
try {
if (m.findSession(sessionId) != null) {
found = true;
break;
}
} catch (IOException e) {
// Ignore. Problems with this manager will be
// handled elsewhere.
}
}
}
if (!found) {
sessionId = null;
}
}
} else {
sessionId = null;
}
// 创建session,如果sessionid是空的话,会创建一个sessionId
session = manager.createSession(sessionId);
//
if (session != null && trackModesIncludesCookie) {
Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie(
context, session.getIdInternal(), isSecure());
// 设置cookie
response.addSessionCookieInternal(cookie);
}
if (session == null) {
return null;
}
session.access();
return session;
}
1.9 六件事之解析用户
这里的用户就是我们登录成功之后放到subject
里面的用户,是我们自定义的类。
我们先看下解析用户的代码,如果获取到的用户不为空就放到context
里面去。
但是这里还没有登录所以这里获取到的用户是空。
protected SubjectContext resolvePrincipals(SubjectContext context) {
PrincipalCollection principals = context.resolvePrincipals();
if (isEmpty(principals)) {
log.trace("No identity (PrincipalCollection) found in the context. Looking for a remembered identity.");
principals = getRememberedIdentity(context);
if (!isEmpty(principals)) {
log.debug("Found remembered PrincipalCollection. Adding to the context to be used for subject construction by the SubjectFactory.");
context.setPrincipals(principals);
} else {
log.trace("No remembered identity found. Returning original context.");
}
}
return context;
}
1.10 六件事之创建Subject
doCreateSubjec
t创建Subject
,这里直接看创建的源码,调用的是DefaultWebSubjectFactory
里面的createSubject
方法创建Subject
这里的逻辑很简单,就是获取到session
、用户、reqeust
、response
,把这些信息作为构造参数new
一个WebDelegatingSubject
对象。
当然这里的session
和用户信息都是空。
public Subject createSubject(SubjectContext context) {
boolean isNotBasedOnWebSubject = context.getSubject() != null && !(context.getSubject() instanceof WebSubject);
// 如果是web这里是false
if (!(context instanceof WebSubjectContext) || isNotBasedOnWebSubject) {
return super.createSubject(context);
}
// 这里其实没做什么,就是new了一个WebDelegatingSubject
WebSubjectContext wsc = (WebSubjectContext) context;
SecurityManager securityManager = wsc.resolveSecurityManager();
Session session = wsc.resolveSession();
boolean sessionEnabled = wsc.isSessionCreationEnabled();
PrincipalCollection principals = wsc.resolvePrincipals();
boolean authenticated = wsc.resolveAuthenticated();
String host = wsc.resolveHost();
ServletRequest request = wsc.resolveServletRequest();
ServletResponse response = wsc.resolveServletResponse();
return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled,
request, response, securityManager);
}
1.11 六件事之保存Subject
保存subject
调用的是DefaultSubjectDAO
的save
方法,然后调用saveToSession
在saveToSession
里面同时调用了mergePrincipals
和mergeAuthenticationState
,因为这里session
是空的所以这两个方法没做什么操作。
我们分别看下这两个方法的源码,从下面的源码中可以看出,这两个方法都是更新session中的用户属性或者登录凭证属性的。
- mergePrincipals
protected void mergePrincipals(Subject subject) {
PrincipalCollection currentPrincipals = null;
// 这里没有登陆所以runAs是false
if (subject.isRunAs() && subject instanceof DelegatingSubject) {
try {
Field field = DelegatingSubject.class.getDeclaredField("principals");
field.setAccessible(true);
currentPrincipals = (PrincipalCollection)field.get(subject);
} catch (Exception e) {
throw new IllegalStateException("Unable to access DelegatingSubject principals property.", e);
}
}
// 如果principals是空,从subject里面获取,当然subject里面也是空的
if (currentPrincipals == null || currentPrincipals.isEmpty()) {
currentPrincipals = subject.getPrincipals();
}
// 从subject获取session,这里传了一个参数false,意思就是只获取,当获取为空也不创建
Session session = subject.getSession(false);
if (session == null) {
// 这里是重点:当session为空用户不为空,还是通过subject.getSession()去获取session,这里没有穿参数,就默认会创建session,这里是登录成功后的逻辑,不过这里在首次拦截的时候不会进这里
if (!isEmpty(currentPrincipals)) {
session = subject.getSession();
// 获取到session之后,把当前属性放到session里面去
session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
}
} else {
// 这里就是session不为空了,先从session里面获取存在的用户
PrincipalCollection existingPrincipals =
(PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (isEmpty(currentPrincipals)) {
// 如果当前用户是空的,session里面的用户不为空,那就要移除掉session里面的用户,可能是退出之类的操作
if (!isEmpty(existingPrincipals)) {
session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
}
} else {
// 如果当前用户和存在的用户不一致,就更新就行
if (!currentPrincipals.equals(existingPrincipals)) {
session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
}
}
}
}
- mergeAuthenticationState
protected void mergeAuthenticationState(Subject subject) {
// 从subject里面获取session,如果session为空也不创建
Session session = subject.getSession(false);
if (session == null) {
// 如果session是空的,但是是登陆过的,isAuthenticated就是判断subject是否是登录过的
// 就再从subject去获取session,这次就要创建了
if (subject.isAuthenticated()) {
session = subject.getSession();
session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);
}
} else {
Boolean existingAuthc = (Boolean) session.getAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
if (subject.isAuthenticated()) {
// 如果subject是登录了的,但是session里面的属性没有,就放进去
if (existingAuthc == null || !existingAuthc) {
session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);
}
} else {
// 如果session里面有登录属性,就移除掉
if (existingAuthc != null) {
session.removeAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
}
}
}
}
🍐2.登录校验流程
登录校验流程从使用Subject
的login
方法开始到自定义的Realm
结束,整个调用过程如下入所示
Subject
登录校验的时候调用Subject
的login
方法,Subject
接口只有一个实现类DelegatingSubject
。
-
DelegatingSubject
login
方法先调用DefaultSecutiryManager
类的login
方法,并返回一个Subject
对象完善
DelegatingSubject
的属性包括登录状态,session
等。 -
DefaultSecurityManager
在这里只干了两件事:
🌿调用认证方法
调用了
AuthenticatingSecurityManager
的authenticate
方法,authenticate
方法返回了AuthenticationInfo
对象。🍀创建subject
调用
createSubject
方法去把用户提交的账号信息封装的AuthenticationToken
、Realm
数据源里面的用户账号信息封装的AuthenticationInfo
、当前Subject
作为参数。
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;
}
这里调用createSubject
方法,和拦截的时候一样了,但是现在有不一样了,这里是有用户主体的,因为这里登录了。
前面我们说了六件事请的保存subject
,这里我们看一下不同的地方,这里获取session
还是空,但是用户主体不为空,因为这里已经登录了。
protected void mergePrincipals(Subject subject) {
PrincipalCollection currentPrincipals = null;
if (subject.isRunAs() && subject instanceof DelegatingSubject) {
try {
Field field = DelegatingSubject.class.getDeclaredField("principals");
field.setAccessible(true);
currentPrincipals = (PrincipalCollection)field.get(subject);
} catch (Exception e) {
throw new IllegalStateException("Unable to access DelegatingSubject principals property.", e);
}
}
if (currentPrincipals == null || currentPrincipals.isEmpty()) {
currentPrincipals = subject.getPrincipals();
}
Session session = subject.getSession(false);
// 这里session为空
if (session == null) {
// 但是用户主体不为空
if (!isEmpty(currentPrincipals)) {
// 这里直接getSession() 回去判断如果session为空,就会创建session
session = subject.getSession();
session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
}
} else {
PrincipalCollection existingPrincipals =
(PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (isEmpty(currentPrincipals)) {
if (!isEmpty(existingPrincipals)) {
session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
}
// otherwise both are null or empty - no need to update the session
} else {
if (!currentPrincipals.equals(existingPrincipals)) {
session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
}
}
}
}
AuthenticatingSecurityManager
authenticate
直接调用AbstractAuthenticator
的authenticate
方法。
-
AbstractAuthenticator
authenticate
方法主要是调用了doAuthenticate
方法,对用户提交的账号密码进行非空校验,并且对调用doAuthenticate
返回结果进行非空校验。 -
ModularRealmAuthenticator
doAuthenticate
方法主要干了一件事就是获取shiro
中已有的数据源Realm
,如果有多个Realm
就调用ModularRealmAuthenticator
,如果是单个Realm
就调用doSingleRealmAuthentication
,最后都会去调用AuthenticatingRealm
的getAuthenticationInfo
方法。Collection realms = getRealms(); if (realms.size() == 1) { return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { return doMultiRealmAuthentication(realms, authenticationToken); }
-
AuthenticatingRealm
getAuthenticationInfo
方法会先从缓存中去查找有没有用户提交的账号信息,如果没有的话,就调用Realm
的doGetAuthenticationInfo
方法。 -
UserRealm
自定义
Realm
就是自己写逻辑,从哪里获取账号密码信息,然后和用户提交的账号密码信息进行校验,校验成功后把用户信息封装成AuthenticationInfo
返回。