本文的单点登录不是cas的,是公司自己做的单点登录系统,主要就是发送特定地址一个带时效的token,特定的方法进行解析处理等,解析好的用户名在当前系统数据库中查询用户相关信息是否存在,包括用户名,密码(SHA1加密不可逆),salt(盐)等信息,如果能查询到该用户,则说明单点登录成功并且在当前系统成功有权避开用户名密码的填写,完成系统的单点登录。
第一次接触shiro框架代理的登录,让我去改造成单点登录,一脸懵逼,然后没办法硬着头皮去做,因为不研究这个系统,根据具体情况,谁也不知道怎么改,别人也没有这个时间精力去管你,所以只能靠自己。
第一个阶段研究了下shiro框架具体流程,具体参照:https://blog.csdn.net/YANGYU1079075086/article/details/78969983?utm_source=blogxgwz0
我参考了网上大部分文章,就觉得他写的最好最透彻也是我这种菜鸟最容易看懂的,边结合代码看,边去按照他写的去代码中找,理解了shiro是怎么控制登录流程的。哪怕看不懂,大致能有个思路,就是这个东西看着眼熟就够了,晚上睡前再想一想。
然后第二个阶段研究了下网上的Shiro整合SSO单点登录系统,具体参照:https://blog.csdn.net/m0_37797991/article/details/78529096
1.虽然大神写的很好,但是并不适用于我的系统,值得庆幸的是他的代码中写了如何避开加盐加密的方法,让我起了反思,成功的解决了这部分问题,也是利用常量,单点登录的写在if里面,else里面是系统原来的密码用户名校验逻辑,我并不想破坏它,那问题来了,如何判断这个请求是来自于哪里的呢,我的设计思路是将密码变成常量,“password”-“salt”的组合,然后在if判断一下是不是这个结构,是不是当前这个用户名对应的加盐加密数值。
2.首先就将单点登录的密码改造为常量
3
那么问题来了,如何单点登录,并且不破环系统原有的代码逻辑,我思考了两种方案。
第一种就是放开一个通道,过滤器放开要解析token的Controller方法,然后重定向到登录页面后台发请求模拟登录,当然估计是忙傻了,这种怎么可能好使,因为我系统之前是有过滤器的,就造成了,哪怕我有session也没有跳到我想要的画面,又被系统之前的过滤器拦截了,走输密码登录的那些逻辑了,这样它造成死循环了,还以为就差临门一脚呢,结果一细想,白忙活了。
第二种我想了一下,因为是被拦截我就想能不能在我们之前登录一系列逻辑加过滤器的前面加一个过滤器,在里面实现登录以后该有的session还shiroUser什么的,这样继续走余下的过滤器,它系统机制肯定能通过校验成功登录到后台。完全有可能,想到这里兴奋的搓了搓手,开动。
4.想是这么想但是还是不会怎么改配置文件呢?Shiro.xml,一看就脑袋疼,哎呀妈呀脑瓜疼,一看到配置文件就脑瓜疼,疼也得看啊,就看,参考上面那个大神的配置的代码,我发现写的是挺好的,但是吧,我这个配置文件中自己原来就有个自定义过滤器,也有对应的realm,本来我也想再加一个自定义过滤器,加个realm的,自定义过滤器加成功了,但是realm说死不走我自己的。
就是这样配的,后来发现其实不加realm也行。走过滤器就行,创建好session后它会继续走之前项目realm中的该有的那些逻辑。
所以不加也行。
然后还参考了下别人写的那个Shiro+cas单点登录的配置文件,最好生成了我自己的配置文件。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
<!-- see: http://fivefish.iteye.com/blog/962457 -->
<description>Shiro 配置</description>
<bean id="sessionFactory" class="com.xxx.activiti.base.shiro.SessionRecordFactory">
</bean>
<bean id="shiroActiveSessionCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<!-- <property name="blocking" value="true" /> -->
<property name="cacheManager">
<ref bean="ehCacheManagerFactory" />
</property>
<property name="cacheName">
<value>shiroActiveSessionCache</value>
</property>
</bean>
<bean id="sessionDao" class="com.xxx.activiti.base.dao.SessionDaoImpl">
<property name="sessionCache" ref="shiroActiveSessionCache"/>
<property name="sessionRecordDao" ref="sessionRecordDao"/>
</bean>
<bean id="nativeSessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="globalSessionTimeout" value="7200000"/>
<property name="sessionFactory" ref="sessionFactory"/>
<property name="sessionDAO" ref="sessionDao"/>
</bean>
<!-- Shiro's main business-tier object for web-enabled applications -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref ="shiroDbRealm" />
<property name="cacheManager" ref="shiroEhcacheManager" />
<property name="sessionManager" ref="nativeSessionManager" />
</bean>
<!-- 自定义的Realm数据源 -->
<bean id="shiroDbRealm" class="com.xxx.activiti.base.shiro.ShiroDbRealm"
depends-on="operationLogDao,commonConfigDao,userDao,sessionRecordDao,userService,commonConfigService">
<property name="authorizationCacheName" value="shiroAuthorizationCache" />
</bean>
<bean id="shiroFilter" class="com.xxx.base.shiro.ShiroFilterFactoryBean">
<!-- shiro的核心安全接口 -->
<property name="securityManager" ref="securityManager" />
<property name="filters">
<util:map>
<entry key="ssoFilter" value-ref="ssoFilter"/><!-- 自定义的ssoFilter要放到最前边 -->
<entry key="authc" value-ref="formAuthenticationFilter" />
<entry key="user" value-ref="userFilter" />
<entry key="logout" value-ref="logoutFilter" />
</util:map>
</property>
<!-- 要求登录时的链接 -->
<property name="loginUrl" value="/login/" />
<!-- 登录成功后要跳转的连接 -->
<property name="successUrl" value="/" />
<!-- 未授权时要跳转的连接 -->
<property name="unauthorizedUrl" value="/no_permission" />
<!-- shiro连接约束配置 -->
<property name="filterChainDefinitions">
<!-- anon 匿名过滤器 authc 如果继续操作,需要做对应的表单验证否则不能通过 authcBasic 基本http验证过滤,如果不通过,跳转屋登录页面
logout 登录退出过滤器 noSessionCreation 没有session创建过滤器 perms 权限过滤器 port 端口过滤器,可以设置是否是指定端口如果不是跳转到登录页面
rest http方法过滤器,可以指定如post不能进行访问等 roles 角色过滤器,判断当前用户是否指定角色 ssl 请求需要通过ssl,如果不是跳转回登录页
user 如果访问一个已知用户,比如记住我功能,走这个过滤器 -->
<value>
/jwt/sso/login = ssoFilter <!--自定义的过滤器-->
/login = anon
/login/ = authc
/logout/ = logout
/s/** = anon
/i18n/**=anon
/robots.txt = anon
/400 =anon
/404 =anon
/500 =anon
/reset =anon
/change =anon
/error/exception/=anon
/** = user
/model/** = anon
*.html = anon
</value>
</property>
</bean>
<!-- shiro 的缓存管理,使用spring的ehCacheManagerFactory, 操作spring缓存工厂来及时更新shiro的缓存管理
ehCacheManagerFactory对象在applicationContext.xml中配置 -->
<bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManager" ref="ehCacheManagerFactory" />
</bean>
<!-- shiro为集成spring -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
<!-- shiro增加验证码支持 -->
<bean id="ssoFilter" class="com.xxx.activiti.base.shiro.SSOFilter" />
<bean id="formAuthenticationFilter" class="com.xxx.activiti.base.shiro.CaptchaFormAuthenticationFilter"/>
<bean id="logoutFilter" class="com.xxx.activiti.base.shiro.LogoutFilter" />
<bean id="userFilter" class="com.xxx.activiti.base.shiro.UserFilter" />
</beans>
package com.xxx.activiti.base.shiro;
import com.idsmanager.dingdang.jwt.DingdangUserRetriever;
import com.xxx.activiti.entity.user.User;
import com.xxx.activiti.user.service.UserService;
import com.xxx.base.ClientInfoHolder;
import com.xxx.base.model.ClientInfo;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import javax.annotation.Resource;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Locale;
public class SSOFilter implements Filter {
@Autowired
private UserService userService;
@Resource
private ShiroDbRealm shiroDbRealm;
@Autowired
private ShiroUserService shiroUserService;
@Value("${sso.public.key}")
private String publicKey;
public static final String KEY_PARAM_CAPTCHA = "kaptcha_login";
protected final transient Logger logger = LoggerFactory.getLogger(getClass());
private boolean isExistedUsername(String userName) {
User user = userService.findByItalentId(userName);
if(user != null) {
return true;
}
return false;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ShiroUser su = (ShiroUser) SecurityUtils.getSubject().getPrincipal();
// 如果用户已处于登录状态则redirect到根目录
if (su != null && su.getUserId().longValue() > 0) {
request.setAttribute("success","成功");
chain.doFilter(request,response);
}else {
HttpServletRequest httpRequests = (HttpServletRequest) request;
HttpServletResponse httpResponses = (HttpServletResponse)response;
DingdangUserRetriever.User user = null;//整个try块中的内容就是解析token,可以用自己公司的代码替换掉
try {
String token = httpRequests.getParameter("id_token");
DingdangUserRetriever retriever = new DingdangUserRetriever(token, publicKey);
//2.获取用户信息
user = retriever.retrieve();
if (user == null) {
httpResponses.sendRedirect("/");
return ;
}
} catch (Exception e) {
httpResponses.sendRedirect("/");
return ;
}
//3.判断用户名是否在自己系统存在isExistedUsername()方法为业务系统自行判断数据库中是否存在
if (isExistedUsername(user.getSub())) {
User userDetail = userService.findByItalentId(user.getSub());
//以下内容是看项目源码中对比未登录前与登录过程中的代码中的值进行new与赋值的。
//部分没有什么用不过不加会判空校验过不去,比如说那个什么电脑客户端的什么信息ClientInfo的那些
String languageStr = WebUtils.getCleanParam(request, "language");
if (languageStr == null || languageStr.length() == 0) {
languageStr = "zh_CN";
}
String[] languageArr = languageStr.split("_");
Locale language = null;
if (languageArr.length == 1) {
language = new Locale(languageArr[0]);
} else {
language = new Locale(languageArr[0], languageArr[1]);
}
CookieLocaleResolver localeResolver = new CookieLocaleResolver();
localeResolver.setCookieName("locale");
localeResolver.setLocale((HttpServletRequest) request,
(HttpServletResponse) response, language);
//很有用的代码,就是创建具体是哪个用户登录的ShiroUser相关信息。密码用加密过的加上加盐
//的信息组成的常量。到后面CaptchaFormAuthenticationFilter过滤器和userFilter过滤器可以顺利通过。
CaptchaFormAuthenticationFilter.UsernamePasswordCaptchaToken t = new CaptchaFormAuthenticationFilter.UsernamePasswordCaptchaToken(userDetail.getUsername(), userDetail.getPassword() + "-" + userDetail.getSalt(), false,
request.getRemoteHost(), WebUtils.getCleanParam(request, KEY_PARAM_CAPTCHA), "", "/", request.getServerName(), language);
ClientInfo clientInfo = new ClientInfo();
clientInfo.setSessionId(((HttpServletRequest) request).getRequestedSessionId());
clientInfo.setPyid(((HttpServletRequest) request).getSession().getId());
clientInfo.setIp(request.getRemoteHost());
clientInfo.setReferrerUrl("http://localhost:8080/");
clientInfo.setUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0.3 Safari/604.5.6");
ClientInfoHolder.setClientInfo(clientInfo);
SecurityUtils.getSubject().login(t);
//这段也很有用,暗搓搓想不加来的,后来发现不好使,也不知道是干什么的。
if (t.getNext() != null) {
WebUtils.issueRedirect(request, response, t.getNext(), null, false);
}
request.setAttribute("success","成功");
chain.doFilter(request, response);
}else {
httpResponses.sendRedirect("/");
return;
}
}
}
@Override
public void destroy() {
}
}
/**
* context根路径controller
*/
@Controller
@RequestMapping(value = "/")
public class RootController {
@Autowired
private UserService userService;
@Resource
private ShiroDbRealm shiroDbRealm;
@RequestMapping(value = "jwt/sso/login" ,method = RequestMethod.GET)
public String ssoUrl(@RequestParam(required = false) String id_token, String redirect_url, Model model, HttpServletRequest request) {
Object abc = request.getAttribute("success");
if(abc!= null) {
model.addAttribute("success", "成功");
}else {
model.addAttribute("error", "失败");
}
return "index/index";
}
}
//这个CaptchaFormAuthenticationFilter是我们系统原有的在登录前需要走的过滤器。
public class CaptchaFormAuthenticationFilter extends FormAuthenticationFilter {
protected final transient Logger logger = LoggerFactory
.getLogger(getClass());
private static String encodeUrlParam(String str) {
try {
return URLEncoder.encode(str, "UTF-8");
} catch (UnsupportedEncodingException e) {
// 忽略
return null;
}
}
public static final String KEY_PARAM_CAPTCHA = "kaptcha_login";
private String getCaptcha(ServletRequest request) {
return WebUtils.getCleanParam(request, KEY_PARAM_CAPTCHA);
}
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,
ServletResponse response) throws Exception {
UsernamePasswordCaptchaToken t = (UsernamePasswordCaptchaToken) token;
if (t.getNext() != null) {
WebUtils.issueRedirect(request, response, t.getNext(), null, false);
return false;
}
return super.onLoginSuccess(token, subject, request, response);
}
@Override
protected AuthenticationToken createToken(ServletRequest request,
ServletResponse response) {
String username = getUsername(request);
String password = getPassword(request);
boolean rememberMe = isRememberMe(request);
String host = getHost(request);
String service = WebUtils.getCleanParam(request, "service");
String next = WebUtils.getCleanParam(request, "next");
String captcha = getCaptcha(request);
String domain = request.getServerName();
String languageStr = WebUtils.getCleanParam(request, "language");
if (languageStr == null || languageStr.length() == 0) {
languageStr = "zh_CN";
}
String[] languageArr = languageStr.split("_");
Locale language = null;
if (languageArr.length == 1) {
language = new Locale(languageArr[0]);
} else {
language = new Locale(languageArr[0], languageArr[1]);
}
CookieLocaleResolver localeResolver = new CookieLocaleResolver();
localeResolver.setCookieName("locale");
localeResolver.setLocale((HttpServletRequest) request,
(HttpServletResponse) response, language);
return new UsernamePasswordCaptchaToken(username, password, rememberMe,
host, captcha, service, next, domain, language);
}
@Override
protected boolean onAccessDenied(ServletRequest request,
ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
return executeLogin(request, response);
} else {
return true;
}
} else {
HttpServletRequest req = WebUtils.toHttp(request);
StringBuilder loginUrl = new StringBuilder(getLoginUrl());
if (StringUtils.isNotBlank(req.getRequestURI())) {
loginUrl.append("?");
loginUrl.append(UserFilter.PARAM_NEXT);
loginUrl.append("=");
loginUrl.append(URLEncoder.encode(req.getRequestURI(), "UTF-8"));
if (StringUtils.isNotEmpty(req.getQueryString())) {
loginUrl.append(URLEncoder.encode(
"?" + req.getQueryString(), "UTF-8"));
}
}
WebUtils.issueRedirect(request, response, loginUrl.toString());
return false;
}
}
public static class UsernamePasswordCaptchaToken extends
UsernamePasswordToken {
private static final long serialVersionUID = 3715989977528807192L;
private String captcha;
private String next;
private String service;
private String domain;
private boolean thirdLogin;
private Locale language;
private String dmptoken;
private String thirdtoken;
public void setThirdtoken(String thirdtoken) {
this.thirdtoken = thirdtoken;
}
public String getThirdtoken() {
return thirdtoken;
}
public String getCaptcha() {
return captcha;
}
public String getDomain() {
return domain;
}
public String getNext() {
return next;
}
public String getService() {
return service;
}
public String getDmptoken() {
return dmptoken;
}
public boolean isThirdLogin() {
return this.thirdLogin;
}
public Locale getLanguage() {
return language;
}
public void setLanguage(Locale language) {
this.language = language;
}
public UsernamePasswordCaptchaToken(String username, String password,
boolean rememberMe, String host, String captcha,
String service, String next, String domain, Locale language) {
super(username, password != null ? password.toCharArray() : null,
rememberMe, host);
this.next = next;
this.service = service;
this.captcha = captcha;
this.domain = domain;
this.language = language;
}
@Override
protected void setFailureAttribute(ServletRequest request,
AuthenticationException ae) {
logger.warn("登录过程中出现异常:", ae);
request.setAttribute(getFailureKeyAttribute(), ae.getLocalizedMessage());
}
public static void main(String[] args) {
}
}
package com.xxx.activiti.base.shiro;
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.PostConstruct;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AccountException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.CredentialsException;
import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.authz.permission.PermissionResolver;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort.Direction;
import com.xxx.activiti.base.autoreload.ReloadableConfig;
import com.xxx.activiti.base.entity.SessionRecord;
import com.xxx.activiti.base.exception.IncorrectCaptchaException;
import com.xxx.activiti.base.exception.LoginConcurrentLimitException;
import com.xxx.activiti.base.shiro.CaptchaFormAuthenticationFilter.UsernamePasswordCaptchaToken;
import com.xxx.activiti.entity.user.User;
import com.xxx.base.ClientInfoHolder;
import com.xxx.base.util.I18nResourceUtil;
import org.springframework.web.servlet.ThemeResolver;
public class ShiroDbRealm extends AuthorizingRealm {
/**
* 用户名不能为空
*/
public static final String ERROR_420 ="420";
/**
* 密码必填
*/
public static final String ERROR_421 ="421";
/**
* 用户不存在
*/
public static final String ERROR_422 ="422";
/**
* 请输入验证码
*/
public static final String ERROR_423 ="423";
/**
* 该IPxx小时内登录错误已超过xx次, 该IP将被锁定xx小时
*/
public static final String ERROR_424 ="424";
/**
* 该账号xx小时内登录错误已超过xx次, 该账号将被锁定xx小时
*/
public static final String ERROR_425 ="425";
/**
* 验证码错误
*/
public static final String ERROR_426 ="426";
/**
* 密码错误
*/
public static final String ERROR_427 ="427";
/**
* 用户尚未审核通过
*/
public static final String ERROR_428 ="428";
/**
* 用户同时登录了xx次,已达到最大限制
*/
public static final String ERROR_429 ="429";
/**
* 该用户暂时不可用,请联系客服,谢谢
*/
public static final String ERROR_430 ="430";
/**
* 当前登录用户不拥有任何广告主
*/
public static final String ERROR_431 ="431";
/**
* 当前登录用户不拥有任何客户
*/
public static final String ERROR_437 ="437";
/**
* 密码已经三个月未修改,请修改后登录
*/
public static final String ERROR_432 ="432";
/**
* 用户已被锁定
*/
public static final String ERROR_433 ="433";
/**
* 用户已被屏蔽
*/
public static final String ERROR_434 ="434";
/**
* 系统未知错误,请联系管理员
*/
public static final String ERROR_435 ="435";
/**
* 用户未登录邮箱激活
*/
public static final String ERROR_436 = "436";
protected final transient Logger logger = LoggerFactory
.getLogger(getClass());
@Autowired
private ShiroUserService shiroUserService;
/**
* 设定Password校验的Hash算法与迭代次数.
*/
@PostConstruct
public void initCredentialsMatcher() {
/*
* HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(
* UserService.HASH_ALGORITHM);
*/
// CustomCredentialsMatcher matcher = new CustomCredentialsMatcher();
setCredentialsMatcher(new AlwaysPassCredentialsMatcher());
setName(SessionRecord.REALM_NAME_WEB_FORM);
}
/**
* 获取当前登录用户的权限
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
ShiroUser shiroUser = (ShiroUser) principals.getPrimaryPrincipal();
Set<String> permissions = shiroUserService.getAllPermission(shiroUser
.getAccount());
if (!shiroUser.getAccount().equals(shiroUser.getRealAccount())
&& shiroUser.isRemainPerm()) {
permissions.addAll(shiroUserService.getAllPermission(shiroUser
.getRealAccount()));
}
info.addObjectPermissions(resolvePermissions(permissions));
// info.addStringPermissions(permissions);
logger.debug("{}的权限清单为:{}", shiroUser.getAccount(),
StringUtils.join(permissions, ", "));
return info;
}
private Collection<Permission> resolvePermissions(Collection<String> stringPerms) {
Collection<Permission> perms = Collections.emptySet();
PermissionResolver resolver = getPermissionResolver();
if (resolver != null && !CollectionUtils.isEmpty(stringPerms)) {
perms = new LinkedHashSet<Permission>(stringPerms.size());
for (String strPermission : stringPerms) {
Permission permission = resolver.resolvePermission(strPermission);
perms.add(permission);
}
}
return perms;
}
/**
* 用户通过客户端登录时,进行各种业务逻辑的验证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authenticationToken)
throws AuthenticationException {
UsernamePasswordCaptchaToken token = (UsernamePasswordCaptchaToken) authenticationToken;
String username = token.getUsername();
try {
// 用户名密码非空验证
if (StringUtils.isBlank(username)) {
throw new AccountException(I18nResourceUtil.getResource(
"username.cannot.null", token.getLanguage())/* 用户名不能为空 */)
.initErrorCode(ERROR_420);
}
if (token.getPassword() == null || token.getPassword().length == 0) {
throw new CredentialsException(I18nResourceUtil.getResource(
"password.cannot.null", token.getLanguage())/* 密码不能为空 */)
.initErrorCode(ERROR_421);
}
// 如果要求输入验证码,则验证是否正确
Serializable sessionId = SecurityUtils.getSubject().getSession()
.getId();
logger.info("验证sessionId: {} 的图片验证码: {}", sessionId,
token.getCaptcha());
// 验证码判断逻辑,普通登录还会进行账户和IP登录失败次数限制验证,第三方登录和dmp登录不受该限制
// captchaCheckProcessor(token, username, sessionId);
User user = shiroUserService.findByUsername(username);
if (user == null) {
// 根据账号没有查询出用户则用户不存在,提示信息是:用户名或密码错误
String text = I18nResourceUtil.getResource(
"username.or.password.error", token.getLanguage());
shiroUserService.recordLoginLog(username, null, text);
throw new UnknownAccountException(text)
.initErrorCode(ERROR_422);
} else {
// TODO 原本protected AuthenticationInfo
// doGetAuthenticationInfo巨大无比,然后就将代码拆成了两个方法,在提取代码的时候就多了好几个return
// 其实就是返回一个AuthenticationInfo的实例,下一步是要将这个return 去掉
// 在本方法体中new出AuthenticationInfo,而不是多层代码嵌套然后在最深层产生该实例
return doGetAuthenticationInfo(token, user);
}
} catch (AuthenticationException e) {
if (!StringUtils.isBlank(username)) {
shiroUserService.recordLoginFailureLog(username);// 保存一个错误登录日志
}
throw e;
}
}
/**
* 判断用户是否需要输入验证码,若需要,则进行验证码判断
*/
private void captchaCheckProcessor(UsernamePasswordCaptchaToken token,
String username, Serializable sessionId) {
if (shiroUserService.captchaValidateByIp(ClientInfoHolder
.getClientInfo().getIp()) > 0 || shiroUserService
.captchaValidateByAccount(username) > 0) {
if (token.getCaptcha() == null || token.getCaptcha().length() == 0) {
shiroUserService.clearCaptchaLoginVerification(sessionId);
throw new IncorrectCaptchaException(
I18nResourceUtil.getResource("captcha.cannot.null",
token.getLanguage())/* 验证码不能为空 */)
.initErrorCode(ERROR_423);
} else {
validateLoginFailureLimit(token, username, sessionId);
validateCaptcha(token, sessionId);
shiroUserService.clearCaptchaLoginVerification(sessionId);
}
}
}
/**
* 验证一定周期内当前IP和账号登录失败次数是否在限定值范围内
* @param token
* @param username
* @param sessionId
*/
private void validateLoginFailureLimit(UsernamePasswordCaptchaToken token,
String username, Serializable sessionId) {
int failureCap = ReloadableConfig.HOLDER.getConfig()
.getLoginFailureCap();
int checkHours = ReloadableConfig.HOLDER.getConfig()
.getLoginFailureCheckHours();
if (shiroUserService.captchaValidateByIp(ClientInfoHolder.getClientInfo()
.getIp()) > failureCap) {
// 您的IP {}小时内登录错误已超过{}次, 该IP将被锁定{}小时
throw new CredentialsException(I18nResourceUtil.getResource(
"over.gap.ip", token.getLanguage(), checkHours, failureCap,
checkHours)).initErrorCode(ERROR_424);
} else if (shiroUserService.captchaValidateByAccount(username) > failureCap) {
// 该账号{}小时内登录错误已超过{}次, 该账号将被锁定{}小时
throw new CredentialsException(I18nResourceUtil.getResource(
"over.gap.account", token.getLanguage(), checkHours,
failureCap, checkHours)).initErrorCode(ERROR_425);
}
}
/**
* 检查输入的验证码是否正确
* @param token
* @param sessionId
*/
private void validateCaptcha(UsernamePasswordCaptchaToken token,
Serializable sessionId) {
if (!shiroUserService.validateCaptchaLoginVerification(sessionId,
token.getCaptcha())) {
shiroUserService.clearCaptchaLoginVerification(sessionId);
throw new IncorrectCaptchaException(I18nResourceUtil.getResource(
"captcha.error", token.getLanguage())/* 验证码错误 */)
.initErrorCode(ERROR_426);
}
}
private AuthenticationInfo doGetAuthenticationInfo(
UsernamePasswordCaptchaToken token, User user)
throws AuthenticationException {
String username = token.getUsername();
checkActiveAndRemoveProcessor(token, user, username);// 验证用户状态
boolean checkPasswordPass = false;
if(token.isThirdLogin() && !StringUtils.isBlank(token.getThirdtoken())){
checkPasswordPass = true;
}else{
//这个checkPassword就是我之前改造的那个方法。常量那个,在最上边
checkPasswordPass = shiroUserService.checkPassword(user, new String(token.getPassword()));
}
/*
* 判断账号是否已经登录邮箱确认
*/
if (!checkPasswordPass) {
shiroUserService.recordLoginLog(username, false);
throw new IncorrectCredentialsException(
I18nResourceUtil.getResource("username.or.password.error",
token.getLanguage())/* 用户名或密码错误 */)
.initErrorCode(ERROR_427);
} else {
// 密码验证成功后进行的一些业务逻辑控制 包括 用户是否未审核通过
// 用户同时在线白名单逻辑控制、用户三个月未修改密码,登录时强制用户修改代码 若都通过,则登录成功
return checkPasswordSuccessProcessor(token, user, username);
}
}
/**
* 验证密码成功后进行的一些业务逻辑控制,包括:
* 用户同时在线白名单逻辑控制
* 用户三个月未修改密码
* @param token
* @param user
* @param username
* @return
*/
protected AuthenticationInfo checkPasswordSuccessProcessor(
UsernamePasswordCaptchaToken token, User user, String username) {
shiroUserService.recordLoginLog(username, true);
try {
byte[] salt = Hex.decodeHex(user.getSalt().toCharArray());
//shiroUserService.deleteLoginFailure(user);
// 登录成功后,删除用户失败数据,重新开始
// 同一账号同时在线数白名单逻辑控制
//loginConcurrentLimitWhilteAccountProcessor(token, user);
// 用户三个月未修改密码,登录时强制用户修改代码
// 取消不再验证密码三个月过期2017-11-28
//passwordOutDateProcessor(token, user);
//OEM定制 设置账号的themeName
// user = userService.findOne(user.getId());
// user.setChangeLastModifiedField(false);
// user.setTheme(domainThemeMapper.getThemeNameByDomain(token.getDomain()));
// userService.save(user);
// 登录成功
// return new SimpleAuthenticationInfo(new ShiroUser(user, partnerPage
// .getContent().get(0).getId(), token.getLanguage(),
// token.getDomain()), user.getPassword(),
// ByteSource.Util.bytes(salt), getName());
return new SimpleAuthenticationInfo(new ShiroUser(user, token.getLanguage(),token.getDomain()), user.getPassword(),ByteSource.Util.bytes(salt), getName());
} catch (DecoderException e) {
logger.error(ExceptionUtils.getStackTrace(e));
throw new RuntimeException(e);
}
}
/**
* 验证账号的同时在线人数是否符合要求
*/
protected void loginConcurrentLimitWhilteAccountProcessor(
UsernamePasswordCaptchaToken token, User user) {
// 验证该账号同时在线数是否符合限制
int loginConcurrentLimit = ReloadableConfig.HOLDER.getConfig()
.getLoginConcurrentCap();
List<SessionRecord> srs = shiroUserService.getLoginRecord(user.getId());
if (srs.size() >= loginConcurrentLimit) {
// 该账号同时登录了{}次,已达到最大限制
throw new LoginConcurrentLimitException(
I18nResourceUtil.getResource(
"over.gap.account.concurrent",
token.getLanguage(), srs.size()))
.initErrorCode(ERROR_429);
}
}
/**
* 检查账号的激活和删除状态
*/
private void checkActiveAndRemoveProcessor(
UsernamePasswordCaptchaToken token, User user, String username) {
if (!user.isActive()) {
// 用户已被锁定
String text = I18nResourceUtil.getResource("user.inactive",
token.getLanguage());
shiroUserService.recordLoginLog(username, null, text);
throw new LockedAccountException(text).initErrorCode(ERROR_433);
} else if (user.isRemoved()) {
// 用户已被屏蔽
String text = I18nResourceUtil.getResource("user.removed",
token.getLanguage());
shiroUserService.recordLoginLog(username, null, text);
throw new DisabledAccountException(text).initErrorCode(ERROR_434);
}
}
/**
* 清空用户关联权限认证,待下次使用时重新加载。
*
* @param account
*/
public void clearCachedAuthorizationInfo(String account) {
ShiroUser shiroUser = new ShiroUser();
shiroUser.setAccount(account);
shiroUser.setRealAccount(account);
SimplePrincipalCollection principals = new SimplePrincipalCollection(
shiroUser, getName());
clearCachedAuthorizationInfo(principals);
}
/**
* 清空所有关联认证
*/
public void clearAllCachedAuthorizationInfo() {
Cache<Object, AuthorizationInfo> cache = getAuthorizationCache();
if (cache != null) {
cache.clear();
}
}
}
class AlwaysPassCredentialsMatcher implements CredentialsMatcher {
/*
* (non-Javadoc)
*
* @see
* org.apache.shiro.authc.credential.CredentialsMatcher#doCredentialsMatch
* (org.apache.shiro.authc.AuthenticationToken,
* org.apache.shiro.authc.AuthenticationInfo)
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
return true;
}
}