在上篇Shiro系列(一)shiro简介中,我们了解了Shiro的工作原理和组成架构,这篇我们将利用spring 、spring mvc整合shiro,了解shiro的身份认证。
推荐书目:张开涛《跟我学shiro》
一、认证流程
在Shiro中,用户需要提供principals(身份) 和 credentials(证明),一般是用户名/密码来验证用户的身份;
认证流程如图所示:
流程如下:
1、首先调用 Subject.login(token)进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils. setSecurityManager()设置;
2、SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
3、Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自 定义插入自己的实现;
4、Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
5、Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返 回/抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进 行访问。
二、示例解析
Spring+Spring MVC+Shiro
新建一个Web Project(基础框架参考使用IDEA整合Spring+Spring MVC+Mybatis+Maven+Jetty框架的搭建),我们使用maven构建项目,添加shiro的依赖包:
<!--核心包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-cas</artifactId>
<version>1.3.2</version>
</dependency>
<!--缓存包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
<!-- spring结合 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
web.xml配置
在web.xml中 配置:
<!-- spring shiro 过滤器 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Shiro配置文件:spring-shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/data/jpa">
<description>The Configuration of Shiro </description>
<!-- 配置权限管理器(核心) -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="adminRealm"/>
<property name="cacheManager" ref="cacheManager"/>
</bean>
<!-- 继承自AuthorizingRealm的自定义Realm,即指定Shiro验证用户登录的类为自定义的ShiroDbRealm.java -->
<bean id="adminRealm" class="com.swc.base.filter.SystemAuthorizingRealm"/>
<!-- 缓存管理 用户授权/认证信息Cache, 采用redis 缓存 -->
<bean id="cacheManager" class="com.swc.base.pojo.RedisCacheManager"/>
<!-- Shiro主过滤器本身功能十分强大,其强大之处就在于它支持任何基于URL路径表达式的、自定义的过滤器的执行 -->
<!-- Web应用中,Shiro可控制的Web请求必须经过Shiro主过滤器的拦截,Shiro对基于Spring的Web应用提供了完美的支持 -->
<!-- id 必须和web.xml 那种配置的delegatingFilterProxy的<filter-name> 保持一致 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- Shiro的核心安全接口,这个属性是必须的 -->
<property name="securityManager" ref="securityManager"/>
<!-- 要求登录时的链接(可根据项目的URL进行替换),非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面 -->
<property name="loginUrl" value="/login"/>
<property name="successUrl" value="/main"/>
<!-- 登录成功后要跳转的连接(本例中此属性用不到,因为登录成功后的处理逻辑在LoginController里硬编码为main.jsp了) -->
<!-- <property name="successUrl" value="/system/main"/> -->
<!-- 用户访问未对其授权的资源时,所显示的连接 -->
<property name="unauthorizedUrl" value="/login"/>
<!-- Shiro连接约束配置,即过滤链的定义 -->
<!-- 下面value值的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值来的 -->
<!-- anon:anonymous 可以被匿名访问它对应的过滤器里面是空的,什么都没做,这里.do和.jsp后面的*表示参数,比方说login.jsp?main这种 -->
<!-- authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截org.apache.shiro.web.filter.authc.FormAuthenticationFilter -->
<property name="filters">
<map>
<entry key="logout" value-ref="systemLogoutFilter"/>
</map>
</property>
<property name="filterChainDefinitionMap" ref="filterChainDefinitionClass" />
</bean>
<!-- url 权限采用第一次匹配优先的方式-->
<bean id="filterChainDefinitionClass" class="com.swc.base.filter.ShirofilterChainDefinitions" scope="singleton">
<property name="filterChainDefinitions">
<value>
<!--/=anon-->
/login=anon
/logout=logout
/register=anon
/oauth/**=anon
/resources/**=anon
/iridium/testGet=anon
/**=authc
</value>
</property>
</bean>
<bean id="systemLogoutFilter" class="com.swc.base.filter.SystemLogoutFilter">
<property name="redirectUrl" value="/login"/>
</bean>
<!-- 保证实现了Shiro内部lifecycle函数的bean执行,可以自动的调用springioc容器中shiro 备案的生命周期方法 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证 -->
<!-- 配置以下两个bean即可实现此功能 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
</beans>
从配置文件中,我们也可以看出Shiro本身不提供维护用户、权限,而是通过Realm让开发人员自己注入到SecurityManager,从而让SecurityManager能得到合法的用户以及权限进行判断;
自定义系统安全认证实现类
因此我们要自己定义系统安全认证实现类:SystemAuthorizingRealm ,继承AuthorizingRealm ,实现它的两个方法:
doGetAuthenticationInfo()和doGetAuthorizationInfo();
doGetAuthenticationInfo是当我们调用subject.login进行认证的方法;
doGetAuthorizationInfo方法是进行用户授权的时候调用的方法;暂且不讨论;
package com.swc.base.filter;
import com.swc.base.utils.StrUtil;
import com.swc.user.entity.User;
import com.swc.user.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 系统认证实现类
* @Auther: songweichao
* @Date: 2018/12/24 14:51
*/
@Service
public class SystemAuthorizingRealm extends AuthorizingRealm {
/**
* 认证回调函数, 登录时调用
*/
@Resource
private UserService userService;
@Resource
private RoleService roleService;
@Resource
private FunctionService functionService;
/**
* 用户认证
*
* @param authcToken 含登录名密码的信息
* @return 认证信息
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) {
if (authcToken == null)
throw new AuthenticationException("parameter token is null");
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
// 校验用户名密码
String password=String.copyValueOf(token.getPassword());
User user= userService.getUserByName(token.getUsername());
if (user!=null) {
if(!password.equals(user.getPassword())){
throw new IncorrectCredentialsException();
}
//这样前端页面可取到数据
SecurityUtils.getSubject().getSession().setAttribute("user",user);
SecurityUtils.getSubject().getSession().setAttribute("userId",user.getId());
// 注意此处的返回值没有使用加盐方式,如需要加盐,可以在密码参数上加
return new SimpleAuthenticationInfo(user.getId(), token.getPassword(), token.getUsername());
}
throw new UnknownAccountException();
}
/**
* 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用 shiro 权限控制有三种
* 1、通过xml配置资源的权限
* 2、通过shiro标签控制权限
* 3、通过shiro注解控制权限
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 因为非正常退出,即没有显式调用 SecurityUtils.getSubject().logout()
// (可能是关闭浏览器,或超时),但此时缓存依旧存在(principals),所以会自己跑到授权方法里。
if (!SecurityUtils.getSubject().isAuthenticated()) {
doClearCache(principals);
SecurityUtils.getSubject().logout();
return null;
}
if (principals == null) {
throw new AuthorizationException("parameters principals is null");
}
//获取已认证的用户名(登录名)
String userId=(String)super.getAvailablePrincipal(principals);
if(StrUtil.isEmpty(userId)){
return null;
}
/* Set<String> roleCodes=roleService.getRoleCodeSet(userId);
//默认用户拥有所有权限
Set<String> functionCodes=functionService.getAllFunctionCode();*/
/* Set<String> functionCodes=functionService.getFunctionCodeSet(roleCodes);*/
SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
/*authorizationInfo.setRoles(roleCodes);
authorizationInfo.setStringPermissions(functionCodes);*/
return authorizationInfo;
}
}
缓存实现类:
package com.swc.base.pojo;
import com.swc.base.dao.RedisDao;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Resource;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* @Auther: songweichao
* @Date: 2018/12/24 14:51
*/
public class RedisCacheManager implements CacheManager {
private static final Logger logger = LoggerFactory.getLogger(RedisCacheManager.class);
// fast lookup by name map
private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<String, Cache>();
@Resource
private RedisDao redisDao;
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
Cache c = caches.get(name);
if (c == null) {
c = new RedisCache<K, V>(redisDao);
caches.put(name, c);
}
return (Cache<K, V>) c;
}
}
Controller控制器:
package com.swc.user.controller;
import com.swc.base.utils.EncryptUtil;
import com.swc.base.utils.ResultCode;
import com.swc.base.utils.StrUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
/**
* @Auther: songweichao
* @Date: 2018/12/27 11:15
* @Description:
*/
@Controller
public class LoginController {
private static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class);
private final static String MAIN_PAGE = "main";
private final static String LOGIN_PAGE = "login";
@RequestMapping(value = "/login")
private String login(HttpServletRequest request, Model model) {
//model.addAttribute("oAuthServices", oAuthServices.getAllOAuthServices());
//已经登录过,直接进入主页
Subject subject = SecurityUtils.getSubject();
if (subject != null && subject.isAuthenticated()) {
Object authorized = subject.getSession().getAttribute("isAuthorized");
//boolean isAuthorized = Boolean.valueOf(subject.getSession().getAttribute("isAuthorized").toString());
if (authorized != null && Boolean.valueOf(authorized.toString()))
return MAIN_PAGE;
}
String userName = request.getParameter("name");
if (StrUtil.isEmpty(userName)) {
return LOGIN_PAGE;
}
String password = request.getParameter("password");
password = EncryptUtil.getPassword(password, userName);
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
token.setRememberMe(true);
subject = SecurityUtils.getSubject();
String msg;
try {
subject.login(token);
if (subject.isAuthenticated()) {
return MAIN_PAGE;
} else {
return LOGIN_PAGE;
}
} catch (IncorrectCredentialsException e) {
msg = "登录密码错误. ";
model.addAttribute("message", new ResultCode("2", msg));
LOGGER.error(msg);
} catch (ExcessiveAttemptsException e) {
msg = "登录失败次数过多";
model.addAttribute("message", new ResultCode("3", msg));
LOGGER.error(msg);
} catch (LockedAccountException e) {
msg = "帐号已被锁定.";
model.addAttribute("message", new ResultCode("1", msg));
LOGGER.error(msg);
} catch (DisabledAccountException e) {
msg = "帐号已被禁用";
model.addAttribute("message", new ResultCode("1", msg));
LOGGER.error(msg);
} catch (ExpiredCredentialsException e) {
msg = "帐号已过期.";
model.addAttribute("message", new ResultCode("1", msg));
LOGGER.error(msg);
} catch (UnknownAccountException e) {
msg = "帐号不存在 " + token.getPrincipal();
model.addAttribute("message", new ResultCode("1", msg));
LOGGER.error(msg);
} catch (UnauthorizedException e) {
msg = "您没有得到相应的授权!" + e.getMessage();
model.addAttribute("message", new ResultCode("1", msg));
LOGGER.error(msg);
}
return LOGIN_PAGE;
}
}
注: 在LoginController中调用 subject.login 方法进行登录,其会自动委托给 SecurityManager.login 方法进行登录,SecurityManager将所传参数UsernamePasswordToken 传给SystemAuthorizingRealm的AuthenticationInfo()方法,调用userService,从数据库中获取用户名和密码,进行比对;一致返回true,不一致则返回相应AuthenticationException 异常;
在浏览器端访问任意地址:
dbug断点可以看到进入了login方法:
地址栏重定向到/login,进入到登录页面:
输入用户名song和密码111111,可以看到通过验证进入主页,也可输入错误密码,日志打印异常信息;
三、带你出坑
整个过程中,界面上访问没有问题,但在访问任何一个url时,控制台都会报如下错误,但是所有url又能访问正常:
2019-01-15 17:41:51,787 WARN[org.apache.shiro.mgt.AbstractRememberMeManager:449]- There was a failure while trying to retrieve remembered principals. This could be due to a configuration problem or corrupted principals. This could also be due to a recently changed encryption key, if you are using a shiro.ini file, this property would be 'securityManager.rememberMeManager.cipherKey' see: http://shiro.apache.org/web.html#Web-RememberMeServices. The remembered identity will be forgotten and not used for this request.
2019-01-15 17:41:51,789 WARN[org.apache.shiro.mgt.DefaultSecurityManager:609]- Delegate RememberMeManager instance of type [org.apache.shiro.web.mgt.CookieRememberMeManager] threw an exception during getRememberedPrincipals().
org.apache.shiro.crypto.CryptoException: Unable to execute 'doFinal' with cipher instance [javax.crypto.Cipher@724e319e].
at org.apache.shiro.crypto.JcaCipherService.crypt(JcaCipherService.java:462)
at org.apache.shiro.crypto.JcaCipherService.crypt(JcaCipherService.java:445)
at org.apache.shiro.crypto.JcaCipherService.decrypt(JcaCipherService.java:390)
at org.apache.shiro.crypto.JcaCipherService.decrypt(JcaCipherService.java:382)
at org.apache.shiro.mgt.AbstractRememberMeManager.decrypt(AbstractRememberMeManager.java:482)
at org.apache.shiro.mgt.AbstractRememberMeManager.convertBytesToPrincipals(AbstractRememberMeManager.java:419)
at org.apache.shiro.mgt.AbstractRememberMeManager.getRememberedPrincipals(AbstractRememberMeManager.java:386)
at org.apache.shiro.mgt.DefaultSecurityManager.getRememberedIdentity(DefaultSecurityManager.java:604)
at org.apache.shiro.mgt.DefaultSecurityManager.resolvePrincipals(DefaultSecurityManager.java:492)
at org.apache.shiro.mgt.DefaultSecurityManager.createSubject(DefaultSecurityManager.java:342)
at org.apache.shiro.subject.Subject$Builder.buildSubject(Subject.java:846)
at org.apache.shiro.web.subject.WebSubject$Builder.buildWebSubject(WebSubject.java:148)
at org.apache.shiro.web.servlet.AbstractShiroFilter.createSubject(AbstractShiroFilter.java:292)
at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:359)
at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1645)
at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:564)
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:578)
at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:221)
at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1111)
at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:498)
at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:183)
at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1045)
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
at org.eclipse.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:199)
at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:109)
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:98)
at org.eclipse.jetty.server.Server.handle(Server.java:461)
at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:284)
at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:244)
at org.eclipse.jetty.io.AbstractConnection$2.run(AbstractConnection.java:534)
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:607)
at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:536)
at java.lang.Thread.run(Thread.java:745)
Caused by: javax.crypto.BadPaddingException: Given final block not properly padded
at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:966)
at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:824)
at com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:436)
at javax.crypto.Cipher.doFinal(Cipher.java:2165)
at org.apache.shiro.crypto.JcaCipherService.crypt(JcaCipherService.java:459)
... 36 more
从warn信息看出,应该是没有配置RememberMeManager 的缘故,将以下配置添加到spring-shiro.xml中,解决了这个问题。
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="adminRealm"/>
<property name="cacheManager" ref="cacheManager"/>
<!-- 定义RememberMe的管理器 -->
<property name="rememberMeManager" ref="rememberMeManager"/>
</bean>
<!-- 定义RememberMe功能的程序管理类 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<!-- 定义在进行RememberMe功能实现的时候所需要使用到的Cookie的处理类 -->
<property name="cookie" ref="rememberMeCookie"/>
</bean>
<!-- 配置需要向Cookie中保存数据的配置模版(RememberMe) -->
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<!-- 设置Cookie在浏览器中保存内容的名字,由用户自己来设置 -->
<constructor-arg value="MLDNJAVA-RememberMe"/>
<!-- 保证该系统不会受到跨域的脚本操作供给 -->
<property name="httpOnly" value="true"/>
<!-- 定义Cookie的过期时间为一小时 -->
<property name="maxAge" value="3600"/>
</bean>
Shiro系列文章: