最近,项目经理分配的一个任务是:要求根据不同角色身份的用户设计不用的登录界面,同时,用户不能跨登录界面登录。原话,我忘记了,意思是:比如,管理员只能用管理员登录界面登录,普通用户只能用普通用户登录界面的登录。因为,我们的项目,登录时,shiro会对请求进行拦截,并根据绑定的realm完成校验… …现在我就根据代码详细的说明,如果有说错的地方,希望能不吝赐教。
请求被authc拦截,如果状态未登录,就会被跳到登录页面,登录成功后,会继续原请求页面,除非原请求就是successurl,才去successurl
/**
* 表单验证(包含验证码)过滤类
* @author jeeplus
* @version 2014-5-19
*/
@Service
public class EmployeeAuthenticationFilter extends org.apache.shiro.web.filter.authc.FormAuthenticationFilter {
public static final String DEFAULT_CAPTCHA_PARAM = "validateCode";
public static final String DEFAULT_MOBILE_PARAM = "mobileLogin";
public static final String DEFAULT_MESSAGE_PARAM = "message";
private SystemService systemService;
private String captchaParam = DEFAULT_CAPTCHA_PARAM;
private String mobileLoginParam = DEFAULT_MOBILE_PARAM;
private String messageParam = DEFAULT_MESSAGE_PARAM;
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
String username = getUsername(request);
String password = getPassword(request);
if (password==null){
password = "";
}
boolean rememberMe = isRememberMe(request);
String host = StringUtils.getRemoteAddr((HttpServletRequest)request);
String captcha = getCaptcha(request);
boolean mobile = isMobileLogin(request);
String loginType = "user";
return new UsernamePasswordToken(username, password.toCharArray(), rememberMe, host, captcha, mobile,loginType);
}
/**
* 获取系统业务对象
*/
public SystemService getSystemService() {
if (systemService == null){
systemService = SpringContextHolder.getBean(SystemService.class);
}
return systemService;
}
public String getCaptchaParam() {
return captchaParam;
}
protected String getCaptcha(ServletRequest request) {
return WebUtils.getCleanParam(request, getCaptchaParam());
}
public String getMobileLoginParam() {
return mobileLoginParam;
}
protected boolean isMobileLogin(ServletRequest request) {
return WebUtils.isTrue(request, getMobileLoginParam());
}
public String getMessageParam() {
return messageParam;
}
/**
* 登录成功之后跳转URL
*/
public String getSuccessUrl() {
return super.getSuccessUrl();
}
@Override
protected void issueSuccessRedirect(ServletRequest request,
ServletResponse response) throws Exception {
Principal p = UserUtils.getPrincipal();
if (p != null && !p.isMobileLogin()){
WebUtils.issueRedirect(request, response, getSuccessUrl(), null, true);
}else{
//super.issueSuccessRedirect(request, response);//手机登录
AjaxJson j = new AjaxJson();
j.setSuccess(true);
j.setMsg("登录成功!");
j.put("username", p.getLoginName());
j.put("name", p.getName());
j.put("mobileLogin", p.isMobileLogin());
j.put("JSESSIONID", p.getSessionid());
PrintJSON.write((HttpServletResponse)response, j.getJsonStr());
}
}
/**
* 登录失败调用事件
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token,
AuthenticationException e, ServletRequest request, ServletResponse response) {
String className = e.getClass().getName(), message = "";
if (IncorrectCredentialsException.class.getName().equals(className)
|| UnknownAccountException.class.getName().equals(className)){
message = "用户或密码错误, 请重试.";
}
else if (e.getMessage() != null && StringUtils.startsWith(e.getMessage(), "msg:")){
message = StringUtils.replace(e.getMessage(), "msg:", "");
}
else{
message = "系统出现点问题,请稍后再试!";
e.printStackTrace(); // 输出到控制台
}
request.setAttribute(getFailureKeyAttribute(), className);
request.setAttribute(getMessageParam(), message);
return true;
}
}
由于代码比较多,我尽量粘贴全,以保证各位能够有清晰的了解;我会对主要的代码进行解释,其余的就不在过多叙述了。在输入完登录信息后,当我们点击登录,首先,Filter会进行过滤,首先,优先创建一个token(方法是:createToken),相信各位应该都能看懂,不过在此我要解释一下,loginType是我继承org.apache.shiro.authc.UsernamePasswordToken 然后新增的一个属性,因为,我要对不同页面进行辨别,这里,注意,因为我是两个登录页面,所以就需要两个Filter进行分别过滤,但是两者代码几乎相同,唯一不同的就是loginType=“admin”(loginType=“user”),所以就不粘贴了。
/**
* 自定义认证器
*
* @author Sunny
*/
public class LoginRealmAuthenticator extends ModularRealmAuthenticator {
private Map<String,Object> definedRealms;
private SystemService systemService;
public void setDefinedRealms(Map<String, Object> definedRealms) {
this.definedRealms = definedRealms;
}
/**
* 获取系统业务对象
*/
public SystemService getSystemService() {
if (systemService == null){
systemService = SpringContextHolder.getBean(SystemService.class);
}
return systemService;
}
/**
* 根据用户类型判断使用哪个Realm
*/
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
throws AuthenticationException {
super.assertRealmsConfigured();
Realm realm = null;
// 使用自定义Token
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 判断用户类型
if ("admin".equals(token.getLoginType())) {
realm = (Realm) this.definedRealms.get("systemAuthorizingRealm");
} else if ("user".equals(token.getLoginType())) {
realm = (Realm) this.definedRealms.get("loginAuthorizingRealm");
}
return this.doSingleRealmAuthentication(realm, authenticationToken);
}
}
当创建完token后,shiro就会通过认证器(至于为什么会这样,因为,是我配置文件这么配置的),这代码逻辑就比较简单了,就是根据loginType去选择所对应的realm,就不过多解释了。
/**
* 系统安全认证实现类
* @author jeeplus
* @version 2014-7-5
*/
@Service
//@DependsOn({"userDao","roleDao","menuDao"})
public class LoginAuthorizingRealm extends AuthorizingRealm {
private Logger logger = LoggerFactory.getLogger(getClass());
private SystemService systemService;
@Autowired
HttpServletRequest request;
/**
* 认证回调函数, 登录时调用
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) {
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
int activeSessionSize = getSystemService().getSessionDao().getActiveSessions(false).size();
if (logger.isDebugEnabled()){
logger.debug("login submit, active session size: {}, username: {}", activeSessionSize, token.getUsername());
}
boolean mobile = WebUtils.isTrue(request, FormAuthenticationFilter.DEFAULT_MOBILE_PARAM);
// 校验登录验证码
if (!mobile && LoginController.isValidateCodeLogin(token.getUsername(), false, false)){
Session session = UserUtils.getSession();
String code = (String)session.getAttribute(ValidateCodeServlet.VALIDATE_CODE);
if (token.getCaptcha() == null || !token.getCaptcha().toUpperCase().equals(code)){
throw new AuthenticationException("msg:验证码错误, 请重试.");
}
}
// 校验用户名密码
User user = getSystemService().getUserByLoginName(token.getUsername());
if (user != null) {
if (Global.NO.equals(user.getLoginFlag())){
throw new AuthenticationException("msg:该已帐号禁止登录.");
}
if("1".equals(user.getUserType())||"2".equals(user.getUserType())) {
throw new AuthenticationException("msg:只能普通用户登录.");
}
byte[] salt = Encodes.decodeHex(user.getPassword().substring(0,16));
return new SimpleAuthenticationInfo(new Principal(user, token.isMobileLogin()),
user.getPassword().substring(16), ByteSource.Util.bytes(salt), getName());
} else {
return null;
}
}
/**
* 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Principal principal = (Principal) getAvailablePrincipal(principals);
// 获取当前已登录的用户
if (!Global.TRUE.equals(Global.getConfig("user.multiAccountLogin"))){
Collection<Session> sessions = getSystemService().getSessionDao().getActiveSessions(true, principal, UserUtils.getSession());
if (sessions.size() > 0){
// 如果是登录进来的,则踢出已在线用户
if (UserUtils.getSubject().isAuthenticated()){
for (Session session : sessions){
getSystemService().getSessionDao().delete(session);
}
}
// 记住我进来的,并且当前用户已登录,则退出当前用户提示信息。
else{
UserUtils.getSubject().logout();
throw new AuthenticationException("msg:账号已在其它地方登录,请重新登录。");
}
}
}
User user = getSystemService().getUserByLoginName(principal.getLoginName());
if (user != null) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
List<Menu> list = UserUtils.getMenuList();
for (Menu menu : list){
if (StringUtils.isNotBlank(menu.getPermission())){
// 添加基于Permission的权限信息
for (String permission : StringUtils.split(menu.getPermission(),",")){
info.addStringPermission(permission);
}
}
}
// 添加用户权限
info.addStringPermission("user");
// 添加用户角色信息
for (Role role : user.getRoleList()){
info.addRole(role.getEnname());
}
// 更新登录IP和时间
getSystemService().updateUserLoginInfo(user);
// 记录登录日志
LogUtils.saveLog(Servlets.getRequest(), "系统登录");
return info;
} else {
return null;
}
}
@Override
protected void checkPermission(Permission permission, AuthorizationInfo info) {
authorizationValidate(permission);
super.checkPermission(permission, info);
}
@Override
protected boolean[] isPermitted(List<Permission> permissions, AuthorizationInfo info) {
if (permissions != null && !permissions.isEmpty()) {
for (Permission permission : permissions) {
authorizationValidate(permission);
}
}
return super.isPermitted(permissions, info);
}
@Override
public boolean isPermitted(PrincipalCollection principals, Permission permission) {
authorizationValidate(permission);
return super.isPermitted(principals, permission);
}
@Override
protected boolean isPermittedAll(Collection<Permission> permissions, AuthorizationInfo info) {
if (permissions != null && !permissions.isEmpty()) {
for (Permission permission : permissions) {
authorizationValidate(permission);
}
}
return super.isPermittedAll(permissions, info);
}
/**
* 授权验证方法
* @param permission
*/
private void authorizationValidate(Permission permission){
// 模块授权预留接口
}
/**
* 设定密码校验的Hash算法与迭代次数
*/
@PostConstruct
public void initCredentialsMatcher() {
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(SystemService.HASH_ALGORITHM);
matcher.setHashIterations(SystemService.HASH_INTERATIONS);
setCredentialsMatcher(matcher);
}
// /**
// * 清空用户关联权限认证,待下次使用时重新加载
// */
// public void clearCachedAuthorizationInfo(Principal principal) {
// SimplePrincipalCollection principals = new SimplePrincipalCollection(principal, getName());
// clearCachedAuthorizationInfo(principals);
// }
/**
* 清空所有关联认证
* @Deprecated 不需要清空,授权缓存保存到session中
*/
@Deprecated
public void clearAllCachedAuthorizationInfo() {
// Cache
// if (cache != null) {
// for (Object key : cache.keys()) {
// cache.remove(key);
// }
// }
}
/**
* 获取系统业务对象
*/
public SystemService getSystemService() {
if (systemService == null){
systemService = SpringContextHolder.getBean(SystemService.class);
}
return systemService;
}
}
经过认证器验证后,就会找到所对应的Realm,这时,就要执行doGetAuthenticationInfo方法,对登录用户信息进行验证了,首要对非手机登录和验证码登录进行判断,若是,则校验验证码是否正确;然后,通过用户的loginName获取用户信息,若不为空,就对密码进行加密并与数据库中加密密码比较,若,一致,则返回到Filter中,issueSuccessRedirect该方法就代表的校验成功时,会跳转到配置文件中定义的成功界面去;若,校验失败,就到Filter的onLoginFailure中,然后返回Controller定义的失败方法中,将失败信息返回给前端。
配置文件:
<bean name="shiroFilterChainDefinitions" class="java.lang.String">
<constructor-arg>
<value>
/static/** = anon
/userfiles/** = anon
${adminPath}/sys/user/infoCareStatus = anon
${adminPath}/sys/user/validateLoginName = anon
${adminPath}/sys/user/validateMobile = anon
${adminPath}/sys/user/validateMobileExist = anon
${adminPath}/sys/user/resetPassword = anon
${adminPath}/sys/register = anon
${adminPath}/sys/register/registerUser = anon
${adminPath}/sys/register/getRegisterCode = anon
${adminPath}/sys/register/validateMobileCode = anon
${adminPath}/soft/sysVersion/getAndroidVer = anon
${adminPath}/soft/sysVersion/getIosVer = anon
${adminPath}/cas = cas
${adminPath}/login = authc
${adminPath}/loginEmployee = membersAuthc
${adminPath}/logout = anon
${adminPath}/** = user
/act/rest/service/editor/** = perms[act:model:edit]
/act/rest/service/model/** = perms[act:model:edit]
/act/rest/service/** = user
/ReportServer/** = user
value>
constructor-arg>
bean>
<bean id="membersAuthc"
class="com.jeeplus.modules.sys.security.EmployeeAuthenticationFilter">
<property name="loginUrl" value="${adminPath}/loginEmployee" />
<property name="successUrl" value="${adminPath}?loginEmployee" />
bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="${adminPath}/login" />
<property name="successUrl" value="${adminPath}?login" />
<property name="filters">
<map>
<entry key="cas" value-ref="casFilter"/>
<entry key="authc" value-ref="formAuthenticationFilter"/>
<entry key="membersAuthc" value-ref="membersAuthc"/>
map>
property>
<property name="filterChainDefinitions">
<ref bean="shiroFilterChainDefinitions"/>
property>
bean>
<bean id="definedRealms" class="org.springframework.beans.factory.config.MapFactoryBean">
<property name="sourceMap">
<map>
<entry key="systemAuthorizingRealm" value-ref="systemAuthorizingRealm"/>
<entry key="loginAuthorizingRealm" value-ref="loginAuthorizingRealm" />
map>
property>
bean>
<bean id="loginRealmAuthenticator" class="com.jeeplus.modules.sys.security.LoginRealmAuthenticator">
<property name="definedRealms" ref="definedRealms"/>
bean>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="authenticator" ref="loginRealmAuthenticator" />
<property name="realms">
<list>
<ref bean="systemAuthorizingRealm" />
<ref bean="loginAuthorizingRealm" />
list>
property>
<property name="sessionManager" ref="sessionManager" />
<property name="cacheManager" ref="shiroCacheManager" />
bean>
配置文件只是将登录Shiro认证的主要配置写在这里。
web.xml就不在这里贴了,但是,请别忘记将shiroFilter过滤器配置进去,否则,就是,无用功了。
这篇博客要感谢https://blog.csdn.net/z947511564/article/details/52261546?utm_source=itdadao&utm_medium=referral作者以及https://blog.csdn.net/mn1992602/article/details/73527889/作者等等,我是参考了众多博客楼主的思路,在结合我自己项目的实际需求,完成开发的,技术永远就是分享就是进步。