架构设计:登录设计 - Springboot整合shiro及shiro源码阅读

🍅一、shiro

🍑1.介绍

shiro的官网对shiro的介绍如下:apache shiro是一个强大且使用简单的框架,可以用于身份验证、授权、加密、session管理。可以被用来保证任何应用程序的安全,无论是web程序还是手机应用程序。

apache shiro提供了应用程序API,来实现以下四个基础功能:

Authentication:提供用户认证,也就是我们说的登录账号和密码的校验。

Authorization:访问控制,也就是权限控制。

Cryptography:加密,保护和隐藏数据。

Session Managementsession管理。

除了这四个基础的功能之外,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,多了获取ServletRequestServletResponse的方法。

🍇二、shrio使用

🍌1.SimpleAccountRealm

SimpleAccountRealm类有一个SimpleAccount集合,用于存储账号和密码

new一个SecurityManager

SimpleAccountRealm放到SecurityManager,然后再把SecurityManager放到SecurityUtils里面

再通过SecurityUtils获取Subject

调用Subjectlogin方法去校验用户名密码

java 复制代码
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文件,按照下面的格式添加账号和密码。

ini 复制代码
[users]
zhangsan=123
xiaoluo=456

2.2 使用

使用流程和SimpleAccountRealm是一样的,只不过数据源是从ini文件中获取的。

java 复制代码
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不仅要负责连接数据源,还要负责用户名和密码逻辑校验的编写。

java 复制代码
public class MyRealm extends AuthorizingRealm {
    /**
     * 模拟数据库数据
     */
    Map<String, String> 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 使用

ini 复制代码
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.使用总结

① 定义一个RealmRealm的作用就是连接数据源,后续校验用户输入的账号和密码就是和Realm连接的数据源去 做对比。

②定义一个SecurityManager,这个是核心用来管理Subject和各模块交互,把Realm setSecurityManager里面。

③把SecurityManager,再放到SecurityUtils里面去。

④通过SecurityUtils获取Subject,也就是当前用户。

⑤把当前用户账号密码封装成UsernamePasswordToken

⑥把UsernamePasswordToken作为参数调用Subjectlogin方法去校验账号密码正确性,具体怎么校验就在Realm里面。

🍈三、Springboot+realm

🍉1. 文件结构

主页index.html,如果访问主页没有登录就会自动跳转到login.htm页面。

用于登录的login.html页面。

Realm文件定义数据源。

查询数据库的UserService

处理登录的LoginController文件。

shiroConfig配置文件。

这里还需要引入shirojar

xml 复制代码
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring</artifactId>
  <version>1.5.3</version>
</dependency>

🍊2. 代码示例

2.1 index.html

xml 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    登录成功,欢迎来到主页。
</body>
</html>

2.2 Login.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
    <form action="http://localhost:9090/study/login" method="post">
        <input type="text" name="username"/>
        <input type="password" name="password">
        <input type="submit" value="登录">
    </form>
</body>
</html>

2.3 Realm

定义一个Realm,这里的数据源我们从数据库获取账号密码进行校验,授权的逻辑这里就不关注,只关注账号密码校验的逻辑。

java 复制代码
@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用于查询数据,这里模拟从数据库查询。

csharp 复制代码
@Service
public class UserService {
    private List<User> 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的过滤器,主要用于拦截请求,判断用户是否登录。同时还要负责初始化SecurityManagerRealm对象。

java 复制代码
@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> 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异常。

java 复制代码
@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这个类开始。

OncePerRequestFilterdoFilter方法中调用了AbstractShiroFilter类的doFilterInternal方法。

1.2 AbstractShiroFilter

AbstractShiroFilter类的doFilterInternal方法做了三件事

😀三件事情:

🥇把HttpServletRequest封装成ShiroHttpServletRequest;把HttpServletResponse封装成ShiroHttpServletResponse

🥈创建一个Subject对象。

🥉把Subject对象绑定到当前线程上面。

😃细说三件事:

上面的三件事,如果详细说的话其实只有创建Subject是核心,这里我们只看创建Subject的方法逻辑。

这里我们重点看createSubject,这里创建Subject是通过new一个WebSubject接口的内部类Builder,在Builder里面会创建SubjectContext用于保存在整个过程中的所需的变量,这里先放所需的SecurityManager

requestresponse

然后调用buildWebSubject方法来创建一个Subject对象。

java 复制代码
protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
    return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
}

1.3 WebSubject

WebSubjectbuildWebSubject方法逻辑很简单:

  • 调用了父类SubjectbuildSubject创建了一个Subject对象
  • 返回Subject对象
java 复制代码
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也只是干了一件简单的事情,调用SecurityManagercreateSubject方法。

kotlin 复制代码
public Subject buildSubject() {
    return this.securityManager.createSubject(this.subjectContext);
}

这里就是核心代码了,创建session,保存session都是在这里做的。先看下源码

1.5 DefaultSecurityManager

java 复制代码
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就是调用了MapputAll方法。

这里调用的是DefaultWebSecurityManager的copy方法

1.7 六件事之确保SecurityManager

这里的确保是确保新的这个DefaultWebSubject里面有SecurityManager,没有就把当前的SecurityManager放到DefaultSubjectContext

1.8 六件事之解析Session

解析session调用的是DefaultSecurityManager类的resolveSession方法,整个获取session的调用链路如下

  • DefaultSecurityManager-resolveSession
java 复制代码
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
java 复制代码
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
java 复制代码
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
java 复制代码
public HttpSession getSession(boolean create) {
    // 获取session
    Session session = doGetSession(create);
    // 如果session为空就返回空
    if (session == null) {
        return null;
    }
    // 如果不为空就返回HttpSession
    return session.getSession();
}
  • Request-doGetSession
java 复制代码
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里面去。

但是这里还没有登录所以这里获取到的用户是空。

ini 复制代码
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

doCreateSubject创建Subject,这里直接看创建的源码,调用的是DefaultWebSubjectFactory里面的createSubject方法创建Subject

这里的逻辑很简单,就是获取到session、用户、reqeustresponse,把这些信息作为构造参数new一个WebDelegatingSubject对象。

当然这里的session和用户信息都是空。

ini 复制代码
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调用的是DefaultSubjectDAOsave方法,然后调用saveToSession

saveToSession里面同时调用了mergePrincipalsmergeAuthenticationState,因为这里session是空的所以这两个方法没做什么操作。

我们分别看下这两个方法的源码,从下面的源码中可以看出,这两个方法都是更新session中的用户属性或者登录凭证属性的。

  • mergePrincipals
java 复制代码
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
java 复制代码
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.登录校验流程

登录校验流程从使用Subjectlogin方法开始到自定义的Realm结束,整个调用过程如下入所示

Subject

登录校验的时候调用Subjectlogin方法,Subject接口只有一个实现类DelegatingSubject

  • DelegatingSubject

    login方法先调用DefaultSecutiryManager类的login方法,并返回一个Subject对象

    完善DelegatingSubject的属性包括登录状态,session等。

  • DefaultSecurityManager

    在这里只干了两件事:

    🌿调用认证方法

    调用了AuthenticatingSecurityManagerauthenticate方法,authenticate方法返回了AuthenticationInfo对象。

    🍀创建subject

    调用createSubject方法去把用户提交的账号信息封装的AuthenticationTokenRealm数据源里面的用户账号信息封装的AuthenticationInfo、当前Subject作为参数。

java 复制代码
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还是空,但是用户主体不为空,因为这里已经登录了。

java 复制代码
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直接调用AbstractAuthenticatorauthenticate方法。

  • AbstractAuthenticator

    authenticate方法主要是调用了doAuthenticate方法,对用户提交的账号密码进行非空校验,并且对调用doAuthenticate返回结果进行非空校验。

  • ModularRealmAuthenticator

    doAuthenticate方法主要干了一件事就是获取shiro中已有的数据源Realm,如果有多个Realm就调用ModularRealmAuthenticator,如果是单个Realm就调用doSingleRealmAuthentication,最后都会去调用AuthenticatingRealmgetAuthenticationInfo方法。

    scss 复制代码
    Collection<Realm> realms = getRealms();
    if (realms.size() == 1) {
      return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
    } else {
      return doMultiRealmAuthentication(realms, authenticationToken);
    }
  • AuthenticatingRealm getAuthenticationInfo方法会先从缓存中去查找有没有用户提交的账号信息,如果没有的话,就调用RealmdoGetAuthenticationInfo方法。

  • UserRealm

    自定义Realm就是自己写逻辑,从哪里获取账号密码信息,然后和用户提交的账号密码信息进行校验,校验成功后把用户信息封装成AuthenticationInfo返回。

相关推荐
2401_895521348 小时前
SpringBoot Maven快速上手
spring boot·后端·maven
disgare8 小时前
关于 spring 工程中添加 traceID 实践
java·后端·spring
ictI CABL8 小时前
Spring Boot与MyBatis
spring boot·后端·mybatis
小江的记录本10 小时前
【Linux】《Linux常用命令汇总表》
linux·运维·服务器·前端·windows·后端·macos
yhole13 小时前
springboot三层架构详细讲解
spring boot·后端·架构
香香甜甜的辣椒炒肉13 小时前
Spring(1)基本概念+开发的基本步骤
java·后端·spring
白毛大侠14 小时前
Go Goroutine 与用户态是进程级
开发语言·后端·golang
ForteScarlet14 小时前
从 Kotlin 编译器 API 的变化开始: 2.3.20
android·开发语言·后端·ios·开源·kotlin
大阿明14 小时前
SpringBoot - Cookie & Session 用户登录及登录状态保持功能实现
java·spring boot·后端
Binary-Jeff15 小时前
Spring 创建 Bean 的关键流程
java·开发语言·前端·spring boot·后端·spring·学习方法