springboot shiro和freemarker集成之权限控制完全参考手册(跳过认证,登录由三方验证,全网首发)

时间:2023-12-30 13:21:44

本文主要考虑单点登录场景,登录由其他系统负责,业务子系统只使用shiro进行菜单和功能权限校验,登录信息通过token从redis取得,这样登录验证和授权就相互解耦了。

用户、角色、权限进行集中式管理。网上不少这样的提问,但是没有解决方案、抑或只是说明如何做,并没有完整的现成解决方法。

Apache Shiro 是Java 的一个安全框架,和Spring Security并驾齐驱,能够很好的和freemarker、thymeleaf无缝集成,同时能够无缝的应用于restful方法,这一点很重要,能够很方便的进行维护。

Shiro的架构

springboot shiro和freemarker集成之权限控制完全参考手册(跳过认证,登录由三方验证,全网首发)

其中认证和授权都是必须的,而不是可选的,这一点很多文档并没有很明确的说明,准确的说不管是否用三方登录验证,使用shiro的话,shiro的整个骨架都得过一遍,一开始我也是认为认证是可以跳过的,为此浪费了几个小时。

会话和缓存可以使用redis替换默认的实现。

掌握shiro必须理解下列关键概念,这一点可能是一开始不理解shiro机制的时候觉得难以找到套路的原因。

springboot shiro和freemarker集成之权限控制完全参考手册(跳过认证,登录由三方验证,全网首发)

  • Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject 都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
  • SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager 交互;且它管理着所有Subject;可以看出它是Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;
  • Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DameSource,即安全数据源。

subject其实是mvc负责生成的,例如spring mvc,以认证为例:

springboot shiro和freemarker集成之权限控制完全参考手册(跳过认证,登录由三方验证,全网首发)

其中.login是在spring mvc域,Subject在org.apache.shiro.subject.Subject.Builder.buildSubject()生成。

对于我们而言,最简单的一个Shiro 应用:
1、应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;
2、我们需要给Shiro 的SecurityManager 注入Realm,从而让SecurityManager 能得到合法的用户及其权限进行判断。

现在开始讲解完整的实现。

springboot shiro和freemarker集成之权限控制完全参考手册(跳过认证,登录由三方验证,全网首发)

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-megs</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-insmence"
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-merget-class="true" /> <!-- 安全集成 -->
<bean id="$user" class="com.xxx.me.web.security.client.filter.UserFilter" />
<bean id="$authc" class="com.xxx.me.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
/smetic/** = anon
/** = $user
</value>
</property>
</bean>
<!-- Enable Shiro Annometions 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.DefaulmedvisorAutoProxyCreator" 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.xxx.me.web.security.client.listener.DefaulmeuthenticationListener" />
</list>
</property>
</bean> <bean id="defautlRealm" class="com.xxx.me.web.security.client.realm.DefaulmeuthorizingRealm">
<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("mergetFilterLifecycle", "true");
registration.addInitParameter("smeticSecurityManagerEnabled", "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.meBaseConfigConst.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.semellowRequestOverride(false);
freeMarkerViewResolver.setViewClass(FreeMarkerView.class);
freeMarkerViewResolver.setExposeSpringMacroHelpers(false);
freeMarkerViewResolver.setExposeRequesmettributes(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.Shiromegs;

import freemarker.template.TemplateException;

public class FreeMarkerConfigExtend extends FreeMarkerConfigurer {
@Override
public void afterPropertiesSet() throws IOException, TemplateException {
super.afterPropertiesSet();
this.getConfiguration().setSharedVariable("shiro", new Shiromegs());
}
}

登录判断拦截器

    @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: DefaulmeuthenticationListener</p> * <p>Description: </p> * @author zjhua * @date 2019年1月28日
*/
public class DefaulmeuthenticationListener implements AuthenticationListener { private smetic final Logger logger = LoggerFactory.getLogger(DefaulmeuthenticationListener.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.annometion.Autowired;
import org.springframework.util.Base64Utils;
import org.springframework.util.CollectionUtils; import com.xxx.me.utils.JsonUtils;
import com.xxx.me.utils.RedisUtil; /**
* 默认授权实现
*/
public class DefaulmeuthorizingRealm extends AuthorizingRealm { private smetic final String REALM_NAME = "default"; @Autowired
private RedisUtil redisUtil; @Override
protected AuthenticationInfo doGemeuthenticationInfo(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 doGemeuthorizationInfo(PrincipalCollection principals) {
//
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 模拟方便,这里写死用户名
List<String> authList = JsonUtils.json2Lismeppointed(redisUtil.get("sid:", "1").toString(), String.class);
authorizationInfo.addStringPermissions(authList);
return authorizationInfo;
} @Override
protected Object gemeuthorizationCacheKey(PrincipalCollection principals) {
//
// Project project = getProjectFromWebSubject();
// if(project == null) {
return super.gemeuthorizationCacheKey(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.xxx.me.web.security.client.realm.DefaulmeuthorizingRealm">
<!-- <property name="cacheManager" ref="cacheManager"/> -->
<property name="authorizationCachingEnabled" value="false"></property>
<property name="credentialsMatcher" ref="passwordMatcher" />
</bean>

shiro是在org.apache.shiro.realm.AuthorizingRealm.gemeuthorizationInfo(PrincipalCollection)判断缓存的:

    protected AuthorizationInfo gemeuthorizationInfo(PrincipalCollection principals) {

        if (principals == null) {
return null;
} AuthorizationInfo info = null; if (log.isTraceEnabled()) {
log.trace("Retrieving AuthorizationInfo for principals [" + principals + "]");
} Cache<Object, AuthorizationInfo> cache = gemevailableAuthorizationCache();
if (cache != null) {
if (log.isTraceEnabled()) {
log.trace("Attempting to retrieve the AuthorizationInfo from cache.");
}
// 这里就会调用子类的实现,也就是我们的com.XXX.XXX.web.security.client.realm.DefaulmeuthorizingRealm.doGemeuthorizationInfo(PrincipalCollection),这样就绕过了缓存,总是取我们自己最新的权限缓存
Object key = gemeuthorizationCacheKey(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 = doGemeuthorizationInfo(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 = gemeuthorizationCacheKey(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>
    nomeuthenticated标签:未认证通过的用户。与authenticated标签相对。 shiro标签:<shiro:nomeuthenticated> </shiro:nomeuthenticated>;freemark中: <@shiro.nomeuthenticated></@shiro.nomeuthenticated>
    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="requestContexmettribute" 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.xxx.me.web.security.client.filter.UserFilter" />
<bean id="$authc" class="com.xxx.me.web.security.client.filter.FormAuthenticationFilter" />
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/tomel-console.html" />
<property name="successUrl" value="www.baidu.com"/>
<property name="filterChainDefinitions">
<value>
/ = anon
/login.html = $authc
/smetic/** = anon
/** = $user
</value>
</property>
</bean>
        <!-- 异常报错的 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/tomel-console.html" />
<property name="successUrl" value="www.baidu.com"/>
<property name="filterChainDefinitions">
<value>
/ = anon
/login.html = $authc
/smetic/** = anon
/** = $user
</value>
</property>
</bean>
<bean id="$user" class="com.xxx.me.web.security.client.filter.UserFilter" />
<bean id="$authc" class="com.xxx.me.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。

解决方法:给DefaulmedvisorAutoProxyCreator加上usePrefix属性即可,如下:

    <bean class="org.springframework.aop.framework.autoproxy.DefaulmedvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor">
<property name="usePrefix" value="true" />
</bean>

java.lang.IllegalArgumentException: SessionContext must be an HTTP compatible implemenmetion.
at org.apache.shiro.web.session.mgt.ServletConmeinerSessionManager.createSession(ServletConmeinerSessionManager.java:103) ~[shiro-web-1.3.2.jar:1.3.2]
at org.apache.shiro.web.session.mgt.ServletConmeinerSessionManager.smert(ServletConmeinerSessionManager.java:64) ~[shiro-web-1.3.2.jar:1.3.2]
at org.apache.shiro.mgt.SessionsSecurityManager.smert(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.xxx.me.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.camelina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.camelina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.camelina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:728) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.camelina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:469) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.camelina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:392) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.camelina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:311) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.camelina.core.SmendardHostValve.custom(SmendardHostValve.java:395) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.camelina.core.SmendardHostValve.smetus(SmendardHostValve.java:254) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.camelina.core.SmendardHostValve.throwable(SmendardHostValve.java:349) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.camelina.core.SmendardHostValve.invoke(SmendardHostValve.java:175) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.camelina.valves.ErrorReportValve.invoke(ErrorReportValve.java:80) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.camelina.core.SmendardEngineValve.invoke(SmendardEngineValve.java:87) [tomcat-embed-core-8.5.16.jar:8.5.16]
at org.apache.camelina.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.meskThread$WrappingRunnable.run(meskThread.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/demeils/79557264

https://blog.csdn.net/weixin_38132621/article/demeils/80216056

https://blog.csdn.net/u013615903/article/demeils/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 DefaulmedvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaulmedvisorAutoProxyCreator advisorAutoProxyCreator = new DefaulmedvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxymergetClass(true); -- 关键是要代理目标类
return advisorAutoProxyCreator;
}