本文主要考虑单点登录场景,登录由其他系统负责,业务子系统只使用shiro进行菜单和功能权限校验,登录信息通过token从redis取得,这样登录验证和授权就相互解耦了。
用户、角色、权限进行集中式管理。网上不少这样的提问,但是没有解决方案、抑或只是说明如何做,并没有完整的现成解决方法。
Apache Shiro 是Java 的一个安全框架,和Spring Security并驾齐驱,能够很好的和freemarker、thymeleaf无缝集成,同时能够无缝的应用于restful方法,这一点很重要,能够很方便的进行维护。
Shiro的架构
其中认证和授权都是必须的,而不是可选的,这一点很多文档并没有很明确的说明,准确的说不管是否用三方登录验证,使用shiro的话,shiro的整个骨架都得过一遍,一开始我也是认为认证是可以跳过的,为此浪费了几个小时。
会话和缓存可以使用redis替换默认的实现。
掌握shiro必须理解下列关键概念,这一点可能是一开始不理解shiro机制的时候觉得难以找到套路的原因。
- Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject 都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
- SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager 交互;且它管理着所有Subject;可以看出它是Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;
- Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
subject其实是mvc负责生成的,例如spring mvc,以认证为例:
其中.login是在spring mvc域,Subject在org.apache.shiro.subject.Subject.Builder.buildSubject()生成。
对于我们而言,最简单的一个Shiro 应用:
1、应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;
2、我们需要给Shiro 的SecurityManager 注入Realm,从而让SecurityManager 能得到合法的用户及其权限进行判断。
现在开始讲解完整的实现。
maven依赖
<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-aspectj</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>net.mingsoft</groupId> <artifactId>shiro-freemarker-tags</artifactId> <version>1.0.0</version> </dependency>
spring配置文件,application-mvc.xml
<?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:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:p="http://www.springframework.org/schema/p" xmlns:sharding="http://shardingjdbc.io/schema/shardingjdbc/sharding" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.3.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://shardingjdbc.io/schema/shardingjdbc/sharding http://shardingjdbc.io/schema/shardingjdbc/sharding/sharding.xsd" default-lazy-init="true"> <context:property-placeholder location="classpath*:jrescloud.properties" ignore-unresolvable="true" order="1"/> <aop:aspectj-autoproxy proxy-target-class="true" /> <!-- 安全集成 --> <bean id="$user" class="com.hundsun.ta.web.security.client.filter.UserFilter" /> <bean id="$authc" class="com.hundsun.ta.web.security.client.filter.FormAuthenticationFilter" /> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="loginUrl" value="/console.html" /> <property name="filterChainDefinitions"> <value> / = anon /login.html = $authc /static/** = anon /** = $user </value> </property> </bean> <!-- Enable Shiro Annotations for Spring-configured beans. Only run after --> <!-- the lifecycleBeanProcessor has run: --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"> <property name="usePrefix" value="true" /> </bean> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean> <!-- Security Manager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="authenticator" ref="authenticator" /> <property name="realm" ref="defautlRealm" /> </bean> <bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator"> <property name="authenticationListeners"> <list> <bean class="com.hundsun.ta.web.security.client.listener.DefaultAuthenticationListener" /> </list> </property> </bean> <bean id="defautlRealm" class="com.hundsun.ta.web.security.client.realm.DefaultAuthorizingRealm"> <property name="cacheManager" ref="cacheManager"/> <property name="credentialsMatcher" ref="passwordMatcher" /> </bean> <bean id="passwordMatcher" class="org.apache.shiro.authc.credential.PasswordMatcher"> <property name="passwordService" ref="passwordService"/> </bean> <bean id="passwordService" class="org.apache.shiro.authc.credential.DefaultPasswordService"> <property name="hashService"> <bean class="org.apache.shiro.crypto.hash.DefaultHashService"> <property name="hashAlgorithmName" value="MD5"/> </bean> </property> </bean> <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager" /> <bean id="simpleCredentialsMatcher" class="org.apache.shiro.authc.credential.SimpleCredentialsMatcher"/> <bean id="allowAllCredentialsMatcher" class="org.apache.shiro.authc.credential.AllowAllCredentialsMatcher"/> </beans>
spring bean配置
@Configuration @ImportResource(locations = { "classpath*:application-mvc.xml" }) public class BaseWebAppConfig { @Bean public FilterRegistrationBean filterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new DelegatingFilterProxy("shiroFilter")); registration.addUrlPatterns("/*"); registration.addInitParameter("targetFilterLifecycle", "true"); registration.addInitParameter("staticSecurityManagerEnabled", "true"); registration.setName("shiroFilter"); registration.setEnabled(true); return registration; }
@Bean public FreeMarkerConfigurer freemarkerConfig() throws IOException, TemplateException { FreeMarkerConfigExtend configurer = new FreeMarkerConfigExtend(); configurer.setTemplateLoaderPath("classpath:/templates"); configurer.setDefaultEncoding("UTF-8"); Map<String, Object> freemarkerVariables = new HashMap<>(); freemarkerVariables.put("appServiceUrl", env.getProperty(BaseConfig.TaBaseConfigConst.APP_SERVICE_URL)); configurer.setFreemarkerVariables(freemarkerVariables); return configurer; } @ConditionalOnProperty(name = "spring.freemarker.enabled", matchIfMissing = true) public FreeMarkerViewResolver getFreemarkViewResolver() { FreeMarkerViewResolver freeMarkerViewResolver = new FreeMarkerViewResolver(); freeMarkerViewResolver.setCache(false); freeMarkerViewResolver.setSuffix(".html"); freeMarkerViewResolver.setContentType("text/html; charset=UTF-8"); freeMarkerViewResolver.setAllowRequestOverride(false); freeMarkerViewResolver.setViewClass(FreeMarkerView.class); freeMarkerViewResolver.setExposeSpringMacroHelpers(false); freeMarkerViewResolver.setExposeRequestAttributes(false); freeMarkerViewResolver.setExposeSessionAttributes(false); return freeMarkerViewResolver; } }
java bean
import java.io.IOException; import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; import com.jagregory.shiro.freemarker.ShiroTags; import freemarker.template.TemplateException; public class FreeMarkerConfigExtend extends FreeMarkerConfigurer { @Override public void afterPropertiesSet() throws IOException, TemplateException { super.afterPropertiesSet(); this.getConfiguration().setSharedVariable("shiro", new ShiroTags()); } }
登录判断拦截器
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String path = request.getContextPath().length() > 1 ? request.getRequestURI().replace(request.getContextPath(), "") : request.getRequestURI(); // 登录认证,模拟方便这里写死访问页面和用户名/密码 if (path.equals("/console.html")) { SecurityUtils.setSecurityManager(SpringContextHolder.getBean(DefaultWebSecurityManager.class)); //得到Subject及创建用户名/密码身份验证Token(即用户身份/凭证) Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("system","1"); //登录密码 subject.login(token); } }
filter实现
import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.subject.Subject; /** * shiro单点登录认证 * <p>Title: FormAuthenticationFilter</p> * <p>Description: </p> * @author zjhua * @date 2019年1月28日 */ public class FormAuthenticationFilter extends org.apache.shiro.web.filter.authc.FormAuthenticationFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { return true; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { return true; } /** * 登录成功后,将原来的session注销,新增新的session * @param token * @param subject * @param request * @param response * @return * @throws Exception */ protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception{ return true; } }
import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; /** * 用户登录校验过滤器 */ public class UserFilter extends org.apache.shiro.web.filter.authc.UserFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { return true; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { return true; } }
import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationListener; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.subject.PrincipalCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 默认认证实现 * <p>Title: DefaultAuthenticationListener</p> * <p>Description: </p> * @author zjhua * @date 2019年1月28日 */ public class DefaultAuthenticationListener implements AuthenticationListener { private static final Logger logger = LoggerFactory.getLogger(DefaultAuthenticationListener.class); @Override public void onSuccess(AuthenticationToken token, AuthenticationInfo info) { // NOP } @Override public void onFailure(AuthenticationToken token, AuthenticationException ae) { // NOP } @Override public void onLogout(PrincipalCollection principals) { // NOP } }
import java.util.Arrays; import java.util.Collections; import java.util.List; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.crypto.hash.DefaultHashService; import org.apache.shiro.crypto.hash.Hash; import org.apache.shiro.crypto.hash.HashRequest; import org.apache.shiro.crypto.hash.HashService; import org.apache.shiro.crypto.hash.Md5Hash; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.Base64Utils; import org.springframework.util.CollectionUtils; import com.hundsun.ta.utils.JsonUtils; import com.hundsun.ta.utils.RedisUtil; /** * 默认授权实现 */ public class DefaultAuthorizingRealm extends AuthorizingRealm { private static final String REALM_NAME = "default"; @Autowired private RedisUtil redisUtil; @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { char[] pwd = new char[] {'x'}; // HashService hashService = new DefaultHashService();
// 模拟方便,这里写死用户名/密码 return new SimpleAuthenticationInfo("system", new Md5Hash("1"), REALM_NAME); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 模拟方便,这里写死用户名 List<String> authList = JsonUtils.json2ListAppointed(redisUtil.get("sid:", "1").toString(), String.class); authorizationInfo.addStringPermissions(authList); return authorizationInfo; } @Override protected Object getAuthorizationCacheKey(PrincipalCollection principals) { // // Project project = getProjectFromWebSubject(); // if(project == null) { return super.getAuthorizationCacheKey(principals); // } // return principals.getPrimaryPrincipal().toString() + "#" + project.getId(); } }
上述就是完整的代码了,这样shiro就相当于实现了只有权限、没有认证过程,因为我们可以基于token得到认证信息直接完成。
示例:
@RequiresPermissions("order:view") @RequestMapping("/demo/vue-page") public String vuePage(Model m) {
return "/demo/vue-page" }
<@shiro.hasPermission name="order:add"> <el-button size="small" @click="showAddDialog">新增(弹框模式)</el-button> </@shiro.hasPermission>
上述配置完成之后,整个就打通了,但是还存在一个问题,就是在进行权限校验的时候,shiro是把权限保存在org.apache.shiro.cache.MemoryConstrainedCacheManager中,它是JVM本地缓存,这会导致基础系统修改之后,权限无法生效,因为shiro的默认机制是退出然后重新登录才会去取。对此有两种解决方法:一种是自己实现缓存(本来想集成shiro-redis,发现还是自己控制最合适),另外一种是禁用缓存。此处先说明第二种。将defautlRealm改为如下即可:
<bean id="defautlRealm" class="com.hundsun.ta.web.security.client.realm.DefaultAuthorizingRealm">
<!-- <property name="cacheManager" ref="cacheManager"/> -->
<property name="authorizationCachingEnabled" value="false"></property>
<property name="credentialsMatcher" ref="passwordMatcher" />
</bean>
shiro是在org.apache.shiro.realm.AuthorizingRealm.getAuthorizationInfo(PrincipalCollection)判断缓存的:
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) { if (principals == null) { return null; } AuthorizationInfo info = null; if (log.isTraceEnabled()) { log.trace("Retrieving AuthorizationInfo for principals [" + principals + "]"); } Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache(); if (cache != null) { if (log.isTraceEnabled()) { log.trace("Attempting to retrieve the AuthorizationInfo from cache."); }
// 这里就会调用子类的实现,也就是我们的com.XXX.XXX.web.security.client.realm.DefaultAuthorizingRealm.doGetAuthorizationInfo(PrincipalCollection),这样就绕过了缓存,总是取我们自己最新的权限缓存 Object key = getAuthorizationCacheKey(principals); info = cache.get(key); if (log.isTraceEnabled()) { if (info == null) { log.trace("No AuthorizationInfo found in cache for principals [" + principals + "]"); } else { log.trace("AuthorizationInfo found in cache for principals [" + principals + "]"); } } } if (info == null) { // Call template method if the info was not found in a cache info = doGetAuthorizationInfo(principals); // If the info is not null and the cache has been created, then cache the authorization info. if (info != null && cache != null) { if (log.isTraceEnabled()) { log.trace("Caching authorization info for principals: [" + principals + "]."); } Object key = getAuthorizationCacheKey(principals); cache.put(key, info); } } return info; }
这种方式还有一个缺陷就是会导致rpc较多,后面只要实现自己的CacheManager引用本地,然后监听基础应用的Logout事件去更新即可,后面再讲。
Shiro包含的标签
guest标签:验证当前用户是否为“访客”,即未认证(包含未记住)的用户;shiro标签:<shiro:guest></shiro:guest> ;freemark中: <@shiro.guest> </@shiro.guest>
user标签:认证通过或已记住的用户 shiro标签:<shiro:user> </shiro:user> ;freemark中: <@shiro.user> </@shiro.user>
authenticated标签:已认证通过的用户。不包含已记住的用户,这是与user标签的区别所在。 shiro标签:<shiro:authenticated> </shiro:authenticated>;freemark中: <@shiro.authenticated></@shiro.authenticated>
notAuthenticated标签:未认证通过的用户。与authenticated标签相对。 shiro标签:<shiro:notAuthenticated> </shiro:notAuthenticated>;freemark中: <@shiro.notAuthenticated></@shiro.notAuthenticated>
principal标签:输出当前用户信息,通常为登录帐号信息 shiro标签:Hello, <@shiro.principal property="name" /> ;freemarker中: Hello, <@shiro.principal property="name" />, how are you today?
hasRole标签:验证当前用户是否属于该角色 ,shiro标签: <shiro:hasRole name="administrator"> Administer the system </shiro:hasRole> ;freemarker中:<@shiro.hasRole name=”admin”>Hello admin!</@shiro.hasRole>
hasAnyRoles标签:验证当前用户是否属于这些角色中的任何一个,角色之间逗号分隔 ,shiro标签: <shiro:hasAnyRoles name="admin,user,operator"> Administer the system </shiro:hasAnyRoles> ;freemarker中:<@shiro.hasAnyRoles name="admin,user,operator">Hello admin!</@shiro.hasAnyRoles>
hasPermission标签:验证当前用户是否拥有该权限 ,shiro标签: <shiro:hasPermission name="/order:*"> 订单 </shiro:hasPermission> ;freemarker中:<@shiro.hasPermission name="/order:*">订单/@shiro.hasPermission> (一般来说,主要使用这个)
lacksRole标签:验证当前用户不属于该角色,与hasRole标签想反,shiro标签: <shiro:hasRole name="admin"> Administer the system </shiro:hasRole> ;freemarker中:<@shiro.hasRole name="admin">Hello admin!</@shiro.hasRole>
lacksPermission标签:验证当前用户不拥有某种权限,与hasPermission标签是相对的,shiro标签: <shiro:lacksPermission name="/order:*"> trade </shiro:lacksPermission> ;freemarker中:<@shiro.lacksPermission name="/order:*">trade</@shiro.lacksPermission>
其他
使用的环境为Spring MVC+FreeMarker,要在ftl页面中使用contextPath,需要在viewResolver中做如下配置(红色部分):
<bean id="viewResolver" class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
<property name="cache" value="true" />
<property name="prefix" value="" />
<property name="suffix" value=".ftl" />
<property name="exposeSpringMacroHelpers" value="true"/>
<property name="requestContextAttribute" value="rc"></property>
</bean>
这样,在页面中使用${rc.contextPath} 就可获得contextPath。
org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method
没有登录,没有认证信息的原因。
Shiro权限配置错误There is no filter with name 'anno' to apply to chain
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'shiroFilter': FactoryBean threw exception on object creation; nested exception is java.lang.IllegalArgumentException: There is no filter with name 'anno' to apply to chain [/preLogin] in the pool of available Filters. Ensure a filter with that name/path has first been registered with the addFilter method(s).
有可能是顺序的问题(至少笔者碰到的是这样),先配置anno即可,如下:
<!-- 正确,没有问题的 --> <bean id="$user" class="com.hundsun.ta.web.security.client.filter.UserFilter" /> <bean id="$authc" class="com.hundsun.ta.web.security.client.filter.FormAuthenticationFilter" /> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="loginUrl" value="/total-console.html" /> <property name="successUrl" value="www.baidu.com"/> <property name="filterChainDefinitions"> <value> / = anon /login.html = $authc /static/** = anon /** = $user </value> </property> </bean>
<!-- 异常报错的 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="loginUrl" value="/total-console.html" /> <property name="successUrl" value="www.baidu.com"/> <property name="filterChainDefinitions"> <value> / = anon /login.html = $authc /static/** = anon /** = $user </value> </property> </bean> <bean id="$user" class="com.hundsun.ta.web.security.client.filter.UserFilter" /> <bean id="$authc" class="com.hundsun.ta.web.security.client.filter.FormAuthenticationFilter" />
shiro集成进来后,调用API直接404异常
@GetMapping("/user") @RequiresPermissions(value={"user:add","resource:delete"},logical = Logical.OR) public User getUserInfo(@RequestParam(value = "crsKey") String username){ return userService.findByUsername(username); }
如果把RequiresPermissions这行去掉,是可以正常访问的,加上之后就是404。
解决方法:给DefaultAdvisorAutoProxyCreator加上usePrefix属性即可,如下:
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"> <property name="usePrefix" value="true" /> </bean>
java.lang.IllegalArgumentException: SessionContext must be an HTTP compatible implementation.
at org.apache.shiro.web.session.mgt.ServletContainerSessionManager.createSession(ServletContainerSessionManager.java:103) ~[shiro-web-1.3.2.jar:1.3.2]
at org.apache.shiro.web.session.mgt.ServletContainerSessionManager.start(ServletContainerSessionManager.java:64) ~[shiro-web-1.3.2.jar:1.3.2]
at org.apache.shiro.mgt.SessionsSecurityManager.start(SessionsSecurityManager.java:152) ~[shiro-core-1.3.2.jar:1.3.2]
at org.apache.shiro.subject.support.DelegatingSubject.getSession(DelegatingSubject.java:336) ~[shiro-core-1.3.2.jar:1.3.2]
at org.apache.shiro.subject.support.DelegatingSubject.getSession(DelegatingSubject.java:312) ~[shiro-core-1.3.2.jar:1.3.2]
at org.apache.shiro.mgt.DefaultSubjectDAO.mergePrincipals(DefaultSubjectDAO.java:204) ~[shiro-core-1.3.2.jar:1.3.2]
at org.apache.shiro.mgt.DefaultSubjectDAO.saveToSession(DefaultSubjectDAO.java:166) ~[shiro-core-1.3.2.jar:1.3.2]
at org.apache.shiro.mgt.DefaultSubjectDAO.save(DefaultSubjectDAO.java:147) ~[shiro-core-1.3.2.jar:1.3.2]
at org.apache.shiro.mgt.DefaultSecurityManager.save(DefaultSecurityManager.java:383) ~[shiro-core-1.3.2.jar:1.3.2]
at org.apache.shiro.mgt.DefaultSecurityManager.createSubject(DefaultSecurityManager.java:350) ~[shiro-core-1.3.2.jar:1.3.2]
at org.apache.shiro.mgt.DefaultSecurityManager.createSubject(DefaultSecurityManager.java:183) ~[shiro-core-1.3.2.jar:1.3.2]
at org.apache.shiro.mgt.DefaultSecurityManager.login(DefaultSecurityManager.java:283) ~[shiro-core-1.3.2.jar:1.3.2]
at org.apache.shiro.subject.support.DelegatingSubject.login(DelegatingSubject.java:256) ~[shiro-core-1.3.2.jar:1.3.2]
at com.hundsun.ta.interceptor.SecurityInteceptor.preHandle(SecurityInteceptor.java:96) [classes/:?]
at org.springframework.web.servlet.HandlerExecutionChain.applyPreHandle(HandlerExecutionChain.java:133) [spring-webmvc-4.3.10.RELEASE.jar:4.3.10.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:962) [spring-webmvc-4.3.10.RELEASE.jar:4.3.10.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901) [spring-webmvc-4.3.10.RELEASE.jar:4.3.10.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970) [spring-webmvc-4.3.10.RELEASE.jar:4.3.10.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861) [spring-webmvc-4.3.10.RELEASE.jar:4.3.10.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:635) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846) [spring-webmvc-4.3.10.RELEASE.jar:4.3.10.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:728) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:469) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:392) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:311) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.catalina.core.StandardHostValve.custom(StandardHostValve.java:395) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.catalina.core.StandardHostValve.status(StandardHostValve.java:254) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.catalina.core.StandardHostValve.throwable(StandardHostValve.java:349) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:175) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:80) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:799) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1455) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-8.5.16.jar:8.5.16]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [?:1.8.0_171]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [?:1.8.0_171]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.5.16.jar:8.5.16]
at java.lang.Thread.run(Thread.java:748) [?:1.8.0_171]
代码如下:
SecurityUtils.setSecurityManager(SpringContextHolder.getBean(DefaultWebSecurityManager.class)); //得到Subject及创建用户名/密码身份验证Token(即用户身份/凭证) Subject subject = SecurityUtils.getSubject(); if (!subject.isAuthenticated()) { logger.info(sessionBean.getId().toString() + "尚未登录,开始自动登录!"); UsernamePasswordToken token = new UsernamePasswordToken(sessionBean.getId().toString(),sessionBean.getPassword() == null ? "1" : sessionBean.getPassword()); //登录密码 try { subject.login(token); } catch (Exception e) { logger.error("自动登录失败!",e); } }
有时候会报错,有时候不报错,这就比较坑爹了,一开始没有找到规律。经过反复测试重现出来了,当没有权限抛出异常后系统会跳转到error页面,于是又进入preHandler,再次去登录,遂出现该问题,不是https://www.cnblogs.com/ningheshutong/p/6478080.html所述的问题,加上判断如果是/error就不尝试登录,问题就解决,如下。
if (!path.equals("/error")) { SecurityUtils.setSecurityManager(SpringContextHolder.getBean("clientSecurityManager")); // 得到Subject及创建用户名/密码身份验证Token(即用户身份/凭证) Subject subject = SecurityUtils.getSubject(); if (!subject.isAuthenticated()) { logger.info(sessionBean.getId().toString() + "尚未登录,开始自动登录!"); UsernamePasswordToken token = new UsernamePasswordToken(sessionBean.getId().toString(), sessionBean.getPassword() == null ? "1" : sessionBean.getPassword()); // 登录密码 try { subject.login(token); } catch (Exception e) { response.sendRedirect(appWebHomeUrl + "/logout.html"); logger.error("自动登录失败!", e); return false; } } }
使用这种方式还有一个注意点,就是shiro的session超时时间设置,如下所示:
Shiro的Session接口有一个setTimeout()方法,登录后,可以用如下方式取得session
SecurityUtils.getSubject().getSession().setTimeout(1800000);
设置的最大时间,正负都可以,为负数时表示永不超时。
SecurityUtils.getSubject().getSession().setTimeout(-1000l);
默认为1800秒。
参考:
https://blog.csdn.net/qq_26321411/article/details/79557264
https://blog.csdn.net/weixin_38132621/article/details/80216056
https://blog.csdn.net/u013615903/article/details/78781166/
http://shiro.apache.org/
https://www.infoq.com/minibooks/apache-shiro-ee-7
http://shiro.apache.org/webapp-tutorial.html
http://shiro.apache.org/java-authorization-guide.html
http://shiro.apache.org/java-authentication-guide.html
其他异常
在Springboot环境中继承Shiro时,使用注解@RequiresPermissions时无效,也就是似乎@RequestMapping失效了。解决方法:
@Bean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); -- 关键是要代理目标类 return advisorAutoProxyCreator; }