目录
一、从登录到调取数据源
1.获取主体、生成认证token
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
user.getUserName(),
user.getPassword()
);
2.进行登录验证
subject.login(usernamePasswordToken);
3.DelegatingSubject 代理主体
里面有 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;
}
}
4.DefaultSecurityManager 默认安全管理器
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;
}
5.ModularRealmAuthenticator 调取数据源进行身份认证
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);
}
}
// 单数据源认证
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) { // 判断 token 支持
String msg = "Realm [" + realm + "] does not support authentication token [" +
token + "]. Please ensure that the appropriate Realm implementation is " +
"configured correctly or that the realm accepts AuthenticationTokens of this type.";
throw new UnsupportedTokenException(msg);
}
// 通过数据源获取认证需要的数据信息对象(此处由自定义认证数据源返回)
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
String msg = "Realm [" + realm + "] was unable to find account data for the " +
"submitted AuthenticationToken [" + token + "].";
throw new UnknownAccountException(msg);
}
return info;
}
二、自定义数据源
自定义数据源通常直接继承 AuthorizingRealm(授权数据源(抽象)),因为 AuthorizingRealm 直接继承 AuthenticatingRealm(认证数据源(抽象)),这样一次继承能就能重写 AuthorizingRealm 的获取授权信息方法 doGetAuthorizationInfo() 和 AuthenticatingRealm的获取认证信息方法 doGetAuthenticationInfo()
public class UserRealm extends AuthorizingRealm {
@Autowired
private IService service; // 注入业务
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 授权逻辑,如查询角色、权限
info.addRole(String role) // 往 info 对象里装填角色 (String形式)
info.addStringPermission(String permission) // 往 info 对象里装填权限 (String形式)
return info;
}
/**
* 登录认证
* AuthenticationException: 异常抛出用来做登录失败的后续处理
* token: 由登录入口处 Subject.login(AuthenticationToken token) 传入
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token; // 强转子类 UsernamePasswordToken,方便拿到用户名、密码
try {
user = loginService.login(username, password); // 登录逻辑通常写在业务中,直接调用。也可以直接将业务写在此方法中
} catch (CaptchaException e) { // 验证码异常
throw new AuthenticationException(e.getMessage(), e); // 抛出不同异常区分登录失败原因,方便后续处理
} catch (UserNotExistsException e) { // 用户不存在
throw new UnknownAccountException(e.getMessage(), e);
} catch (UserPasswordNotMatchException e) { // 密码不匹配
throw new IncorrectCredentialsException(e.getMessage(), e);
} catch (UserPasswordRetryLimitExceedException e) { // 超出密码次数限制
throw new ExcessiveAttemptsException(e.getMessage(), e);
} catch (UserBlockedException e) { // 用户锁定
throw new LockedAccountException(e.getMessage(), e);
} catch (RoleBlockedException e) { // 用户所在角色锁定
throw new LockedAccountException(e.getMessage(), e);
} catch (Exception e) {
log.info("对用户[" + username + "]进行登录验证..验证未通过{}", e.getMessage());
throw new AuthenticationException(e.getMessage(), e);
}
// 走到这里就是通过了登录认证,登录成功。封装信息对象返回即可
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
return info;
}
}
三、自定义(接驳)过滤器
对于 Shiro 框架中的 AccessControlFilter,用接驳过滤器这个名称非常合适。就像轨道,一个接驳过滤器就是一个三叉轨道,控制数据的走向,多个过滤器连起来就形成了一条过滤器链,一条控制链。
AccessControlFilter 过程控制过滤器**
一般自定义一个过滤器插入 Shiro 框架的过滤链中,通常就是继承 AccessControlFilter 然后实现其控制方法。
1.内部函数
2.主要控制方法(也是需要我们写逻辑的方法)
AccessControlFilter的控制方法主要是指 onPreHandle()、 isAccessAllowed() 和 onAccessDenied() 这三大方法
// 预处理方法,控制请求是否放行的主逻辑
// 先走 isAccessAllowed() 的逻辑,如果返回 true,直接放行,毋需经过 onAccessDenied() 处理
// 如果 isAccessAllowed() 返回 false,则后续经由 onAccessDenied() 处理,返回 true 放行, 返回 false 拦截
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}
一般我们不会动 onPreHandle(),也就是直接用 AccessControlFilter 对 onPreHandle() 的默认实现逻辑;
需要我们自己实现的是isAccessAllowed() 和 onAccessDenied() 这两个抽象方法,只需要写好自己需要的逻辑,用 boolean 返回值控制放行即可
protected abstract boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;
protected abstract boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception;
四、Session处理
会话是 http 请求协议下服务器存储请求信息的载体,通过 Session 可以使 http 请求实现一种 keep alive 的伪长链接
1.java web 框架的请求处理
简化后大概是这个鬼样,在 springboot 框架里把 Servlet 当成 Controller 就好
可见 HttpServletRequest 对象就是个信息载体
2.HttpServletRequest 与 ServletRequest 与 Session 的关系
在 HttpServletRequest 接口中可以发现其继承了 ServletRequest 接口
public interface HttpServletRequest extends ServletRequest {...}
进入 ServletRequest 接口观察其声明的方法没有一个与 Session 有关
而 HttpServletRequest 接口中声明了两个与 Session 相关的方法
public interface HttpServletRequest extends ServletRequest {
...
public HttpSession getSession(boolean create);
public HttpSession getSession();
...
}
也就是说 Session 对象是专门为服务HttpServletRequest 接口而生的,也就是专门服务与 http 请求的,是为了解决 http 单向传递无法保存的缺陷,实现伪长链接而生
3.ShiroHttpServletRequest 对 HttpServletRequest 的实现
在 shiro 框架中,用 ShiroHttpServletRequest 类实现了 HttpServletRequest接口
public class ShiroHttpServletRequest extends HttpServletRequestWrapper {...}
实现了 getSession() 方法
public class ShiroHttpServletRequest extends HttpServletRequestWrapper {
public HttpSession getSession() {
return getSession(true);
}
public HttpSession getSession(boolean create) {
HttpSession httpSession;
if (isHttpSessions()) { // 是否为 HttpSession (默认为 HttpSession)
httpSession = super.getSession(false); // 调取父类的 getSession(),但为空时不生成 session 对象
if (httpSession == null && create) { // session 为空
//Shiro 1.2: assert that creation is enabled (SHIRO-266):
if (WebUtils._isSessionCreationEnabled(this)) { // 允许 session 生成
httpSession = super.getSession(create); // 调取父类的 getSession(),为空时生成 session 对象
} else {
throw newNoSessionCreationException();
}
}
} else {
boolean existing = getSubject().getSession(false) != null;
if (this.session == null || !existing) {
Session shiroSession = getSubject().getSession(create);
if (shiroSession != null) {
this.session = new ShiroHttpSession(shiroSession, this, this.servletContext);
if (!existing) {
setAttribute(REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
}
} else if (this.session != null) {
this.session = null;
}
}
httpSession = this.session;
}
return httpSession;
}
}
重写的 getSession() 方法就是当请求进来时,查询是否有对应的 Session 对象存在,如果有直接返回,没有则创建一个 Session 对象
4.Session的来源与用户绑定
现在知道服务器接收请求后会主动创建一个对应的 Session 对象保存对应的用户数据,但是 Session 是怎么和用户对接上的呢?是怎么准确地取到某个请求对应的 Session 对象呢?
先看 Session 的数据结构,这里的 Session 对象是指 shiro 框架的 ShiroHttpSession 类,它实现了 HttpSession 接口
HttpSession 接口是 Session 的标准接口,规定了一个 Session 应该有的基础方法,不同框架都是基于此接口去实现定义自己的 Session 对象,实现接口、行业的统一对接
先看 HttpSession 定义的基本方法
再看 ShiroHttpSession 里面除了对 HttpSession 的实现,还定义了一个常量
// 默认 session id 名
public static final String DEFAULT_SESSION_ID_NAME = "JSESSIONID";
来了,打开浏览器,访问登录页
可以看到请求头中的 Cookie 属性中包含了一个键值对,key 就是 “JSESSIONID”
和 ShiroHttpSession 中的 DEFAULT_SESSION_ID_NAME 常量的值对应上了
这就是 Session 和用户能对应上的秘密 , 靠的是 Cookie 这鬼东西
5.Cookie 哪里来
清空浏览器 Cookie 记录,重新访问服务
在过滤器中尝试获取请求中携带的 Cookie
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
Cookie[] cookies = httpServletRequest.getCookies();
结果为空,就是第一次请求是没有携带 Cookie 的
再看浏览器控制台,响应头携带上了 JSESSIONID
不登陆,只是刷新登录页,再请求一次
这次请求就携带上 Cookie 了,而且 Cookie 对象里还有个 JSESSIONID
(可以看到 Cookie 就是一个键值对,request 对象是以数组的形式存储 Cookie的)
可以猜测,请求第一次进来,服务端接收到请求就创建了 Session 对象和 Cookie 对象
问题来了,shiro 框架中 Cookie 是什么时候创建的呢
我们追着 DEFAULT_SESSION_ID_NAME 这个常量找
发现 ShiroHttpServletResponse 有引用到这个常量,这个是 shiro 实现的响应对象
结合上面的请求,首先猜测是 Cookie 对象就是在请求进来,服务器创建 ShiroHttpServletResponse 对象的时候,在初始化的过程中完成了 Cookie对象(针对JSESSIONID)的创建
但看了下 ShiroHttpServletResponse 的代码发现不是的
ShiroHttpServletResponse 只有一个构造器,初始化时只是创建了 ServletContext 对象和 ShiroHttpServletRequest 对象 ,并没有做任何与 Cookie 相关的操作
public ShiroHttpServletResponse(HttpServletResponse wrapped, ServletContext context, ShiroHttpServletRequest request) {
super(wrapped);
this.context = context;
this.request = request;
}
而是在其转编码函数中的逻辑才出现了对 DEFAULT_SESSION_ID_NAME 常量的引用,设置 (针对JSSIONID) 的 Cookie
protected String toEncoded(String url, String sessionId) {
//...
StringBuilder sb = new StringBuilder(path);
if (sb.length() > 0) { // session id param can't be first.
sb.append(";");
sb.append(DEFAULT_SESSION_ID_PARAMETER_NAME);
sb.append("=");
sb.append(sessionId);
}
//...
return (sb.toString());
}
也就是说, (针对JSSIONID) 的 Cookie 对象 是在调用 ShiroHttpServletResponse 的编码方法 toEncoded() 设置的。
结论
①请求进来服务时,就会创建对应的 ShiroHttpServletRequest ,ShiroHttpServletResponse 和 ShiroHttpSession 三个对象
②当服务响应返回时,调用 ShiroHttpServletResponse 对象的转URL编码方法,期间把 (针对JSSIONID) 的 Cookie 加入其中,进行响应输出,发送给用户(浏览器)。
③浏览器拿到(针对JSSIONID) 的 Cookie 对象后保存为临时 Cookie,后面的访问将自动带上这个 Cookie