架构设计:登录设计 - 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返回。

相关推荐
Chrikk1 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*1 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue1 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man1 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
customer083 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml44 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
小码编匠5 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#
AskHarries5 小时前
Java字节码增强库ByteBuddy
java·后端