SSM框架整合笔记(九)Shiro介绍

Shiro整体架构

shiro-整体架构-201949225012

  • Authenticator:认证器,管理登录登出;
  • Authorizer:授权器;
  • SessionManager:Session管理器,可以不借助web容器使用session;
  • SessionDAO:session的增删改查;
  • CacheManager:缓存管理机制,缓存角色数据和权限数据;
  • Realm:Shiro和数据库数据源之间的桥梁,shiro通过Realm获取角色数据,权限数据;
  • cryptography:加密。

主体提交请求->调用Authenticator->通过Realm获取认证信息;

Shiro基本概念

如上文说明,认证(Authentication)和授权(Authorization)特别像的两个词,我自己的理解是认证简单说就是证明王大锤是王大锤,授权就是说明王大锤的职能,比如砸墙。如有不当之处,请通过评论或公众号等联系方式联系笔者纠正,谢谢。

Shiro认证

认证过程:

  1. 创建SecurityManager;
  2. 主体提交认证;
  3. SecurityManager认证;
  4. Authenticator认证;
  5. Realm验证;

Shiro授权

  1. 创建SecurityManager;
  2. 主体授权;
  3. SecurityManager授权;
  4. Authorizer授权;
  5. Realm获取角色权限数据;

内置Realm

  1. 内置Realm:
    a. IniRealm
    b. JdbcRealm
    * 在不设置查询语句的时候,默认有查询语句;
    * 权限查询开关默认为关闭,需要打开;

自定义Realm

参考下文的具体代码

Shiro加密

  1. Shiro散列配置
    • HashedCredentialsMatcher
    • 自定义Realm中使用散列
    • 盐的使用

Shiro集成Spring

Shiro基本配置与用户登录

一、添加shiro相关依赖

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<shiro.version>1.3.2</shiro.version>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
二、配置Shiro过滤器

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- ============Shiro过滤器start============-->
<filter>
<description>shiro 权限拦截</description>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- ============Shiro过滤器end============-->
三、添加Shiro的配置
  • 配置shiroFilter过滤器shiroFilter;
  • 自定义的表单过滤器、角色、权限过滤器sysRolesFilter;
  • 配置权限管理器securityManager
  • 自定义的userRealm

spring-shiro.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<description>Shiro配置</description>
<!-- shiro的过滤器工厂,id需要配置的和web.xml中配置的一样 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<!--配置登录请求地址-->
<property name="loginUrl" value="/index"/>
<!--配置成功认证后跳转的url-->
<property name="successUrl" value="/sys/home"/>
<!--认证未成功【无权限访问】跳转的界面,可配置为403请求地址-->
<property name="unauthorizedUrl" value="/error403"/>
<!--引入自己定义的重写了表单过滤器的过滤器-->
<property name="filters">
<map>
<entry key="authc" value-ref="formAuthenticationFilter"/>
<entry key="roles" value-ref="sysRolesFilter"/>
</map>
</property>
<!-- 将权限配置为动态加载 -->
<property name="filterChainDefinitionMap" ref="chainDefinitionSectionMetaSource"></property>
</bean>

<bean class="com.weyoung.platform.shiro.filter.SysRolesFilter" id="sysRolesFilter"/>

<bean id="chainDefinitionSectionMetaSource"
class="com.weyoung.platform.shiro.service.ChainDefinitionSectionMetaSource">
<!-- 定义默认的URL权限 -->
<property name="filterChainDefinitions">
<value>
<!-- anon表示此地址不需要任何权限即可访问 -->
/=anon
/assets/**=anon
/webservice/**=anon
/view/system/**=anon
/view/templates/**=anon
/sys/login=anon
/error403=anon
/swagger-ui.html=anon
/webjars/**=anon
/druid/**=authc
/errorException=anon
/sys/logout=anon
<!--/sys/home=anon-->
/sys/home=authc
/sys/home=roles["admin"]
/**=authc
</value>
</property>
</bean>

<!-- 缓存管理器 使用Ehcache实现 -->
<bean id="shiroCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManager" ref="ehCacheManager"/>
</bean>

<!-- 权限管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm"/>
<property name="cacheManager" ref="shiroCacheManager"/>
</bean>

<!-- 自定义用于授权和认证的realm,init-method:初始化方法 -->
<bean id="userRealm" class="com.weyoung.platform.shiro.realm.SystemAuthRealm">
<!-- 定义需要缓存的认证数据、授权数据,缓存区的名字就是ehcache.xml 自定义cache的name -->
<property name="authorizationCacheName" value="authCache"></property>
</bean>

<!--重写的表单过滤器-->
<bean id="formAuthenticationFilter" class="com.weyoung.platform.shiro.filter.MyFormAuthenticationFilter">
<property name="loginUrl" value="/"/>
</bean>

spring-mvc.xml

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 保证实现了Shiro内部lifecycle函数的bean执行,不能和mvc的配置分开,否则不生效 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

<!-- AOP式方法级权限检查 ,启用了ioc容器中使用shiro注解 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true"/>
</bean>

<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
四、自定义Realm

主要用于从数据库中动态加载角色、权限信息
shiro并不是在认证之后就马上对用户授权,而是在用户认证通过之后,
接下来要访问的资源或者目标方法需要权限的时候才会调用doGetAuthorizationInfo()方法,进行授权.

Realm:域,是Shiro和应用程序之间的连接器。Shiro从Realm获取权限数据(如用户、角色、权限及其之间的关系),SecurityManager验证用户身份时,需要使用Realm的认证器确定用户身份合法,使用Realm的授权器获取用户的角色和权限。

在前端发起登录请求时,执行subject.login(token);代码时,

本文使用示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* 用户验证
* 通过账户信息返回认证信息
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
logger.debug("------------Shiro开始验证------------");
// 1.获取登录的username
String userName = (String) token.getPrincipal();
// 2.根据用户输入的账号username获取用户的信息
UserLogin userLogin = loginDao.getUserLoginByUserName(userName);
// 是否此用户是否存在
if (userLogin == null) {
throw new UnknownAccountException(LOGIN_PASS_ERROR_MSG);
} else {
password = userLogin.getPassword();
}
// 检测账号是否被锁定
// if (Boolean.TRUE.equals(userLogin.getLocked())) {
// throw new LockedAccountException();
// }
String credentials = password;
// 3. 设置密码盐值
// String salt = "weyoung";
// ByteSource credentialsSalt = new Md5Hash(salt);
String realmName = getName();
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userName,
credentials,
realmName);
// 4. 身份认证成功,返回认证信息
logger.debug("------------Shiro完成验证------------");
return info;
}
/**
* 授权并返回权限信息
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
logger.debug("------------Shiro开始授权------------");
// 1.从shiro中取出用户对象
String userName = (String) principalCollection.getPrimaryPrincipal();
// 2.从数据库或者缓存中加载用户的角色标识和权限列表
List<SysRolePermission> rolePermissions = this.getRolesByUserName(userName);
List<String> roleKeys = rolePermissions.stream().map(SysRolePermission::getRoleKey).distinct().collect(Collectors.toList());
List<String> permissions = rolePermissions.stream().map(SysRolePermission::getPerms).distinct().collect(Collectors.toList());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(permissions);
info.addRoles(roleKeys);
logger.debug("------------Shiro完成授权------------");
return info;
}

五、登录实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RequestMapping(value = "/sys/login", method = RequestMethod.POST)
public String systemLogin(HttpServletRequest request, HttpServletResponse response) throws Exception {
String userName = request.getParameter("userName");
String password = request.getParameter("password");
Subject currentUser = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
String viewName = REDIRECT_VIEW_URL_INDEX;
try {
// 这块执行shiro的登录,进入上面描述的自定义的Realm中的认证方法中
currentUser.login(token);
} catch (AuthenticationException e) {
if (e instanceof IncorrectCredentialsException) {
logger.error("登录失败!{}", LOGIN_PASS_ERROR_MSG);
}
return viewName;
}
// 获取用户信息存入session,通过Shiro管理的session,要获取依然必须通过Shiro
UserInfo userInfo = loginService.getSysUserByUserName(userName);
currentUser.getSession().setAttribute(SESSION_DEFAULT, userInfo);
// 登录成功
return REDIRECT_VIEW_URL_HOME;
}
六、Shiro过滤器
  1. Shiro内置过滤器
    • 认证相关的过滤器:anon:不需要任何认证,authBasic:,authc:需要认证后访问,user,logout
    • perms,roles,ssl,port

添加过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class RolesOrFilter extends AuthorizationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
Subject subject = getSubject(servletRequest, servletResponse);

String[] roles = (String[]) o;
if (roles == null || roles.length == 0) {
return true;
}

for (String role : roles) {
if (subject.hasRole(role)) {
return true;
}
}
return false;
}
}
七、通过注解方式进行授权
  • @RequiresRoles:都可以传入多个参数
  • @RequiresPermissions:

pom中引入

1
2
3
4
5
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>

如果需要实现通过注解配置授权,需要在spring-mvc.xml中添加如下代码。
注意:以下的注解代码尽量不要和springMVC的配置分开,否则会不生效

1
2
3
4
5
<aop:config proxy-target-class="true">
<bean class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>

controller中添加示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RequiresRoles("admin")
@RequestMapping(value = "/testRole", method = RequestMethod.GET)
@ResponseBody
public String testRole() {
System.out.println("testRole");
return "testRole success";
}

@RequiresPermissions("admin1")
@RequestMapping(value = "/testRole1", method = RequestMethod.GET)
@ResponseBody
public String testRole1() {
System.out.println("testRole1");
return "testRole1 success";
}

Shiro数据库表结构设计

shiro-数据库表设计-201951111162

Shiro会话管理和缓存管理

Shiro会话(Session)管理

  1. shiro session 管理
    • SessionManager:session管理器、SessionDAO:session增删改查
    • 通过Redis实现session共享
    • Redis实现session共享存在的问题

pom中添加:

1
2
3
4
5
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>

添加RedisSessionDao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class RedisSessionDao extends AbstractSessionDAO {

@Resource
private JedisUtil jedisUtil;

private final String SHIRO_SESSION_PREFIX = "lucifer-session:";

private byte[] getKey(String key) {
return (SHIRO_SESSION_PREFIX + key).getBytes();
}

private void saveSession(Session session) {
if (session != null && session.getId() != null) {
byte[] key = getKey(session.getId().toString());
byte[] value = SerializationUtils.serialize(session);
jedisUtil.set(key, value);
jedisUtil.expire(key, 600);
}
}

@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
// 对session和sessionId进行捆绑
assignSessionId(session, sessionId);
saveSession(session);
return sessionId;
}

@Override
protected Session doReadSession(Serializable sessionId) {
System.out.println("doReadSession");
if (sessionId == null) {
return null;
}
byte[] key = getKey(sessionId.toString());
byte[] value = jedisUtil.get(key);
return (Session) SerializationUtils.deserialize(value);
}

@Override
public void update(Session session) throws UnknownSessionException {
saveSession(session);
}

@Override
public void delete(Session session) {
if (session == null || session.getId() == null) {
return;
}
byte[] key = getKey(session.getId().toString());
jedisUtil.del(key);
}

@Override
public Collection<Session> getActiveSessions() {
Set<byte[]> keys = jedisUtil.keys(SHIRO_SESSION_PREFIX);
Set<Session> sessions = new HashSet<>();
if (CollectionUtils.isEmpty(keys)) {
return sessions;
}
for (byte[] key : keys
) {
Session session = (Session) SerializationUtils.deserialize(jedisUtil.get(key));
sessions.add(session);
}
return sessions;
}
}

添加spring-redis.xml文件,内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean class="redis.clients.jedis.JedisPool" id="jedisPool">
<constructor-arg ref="jedisPoolConfig"/>
<constructor-arg value="127.0.0.1"/>
<constructor-arg value="6379"/>
</bean>

<bean class="redis.clients.jedis.JedisPoolConfig" id="jedisPoolConfig"/>
</beans>

spring.xml中添加如下代码;

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 使用自定义的sessionManager -->
<bean class="com.lucifer.session.CustomSessionManager" id="sessionManager">
<property name="sessionDAO" ref="redisSessionDao"/>
</bean>

<bean class="com.lucifer.session.RedisSessionDao" id="redisSessionDao"/>

<!--创建securityManager对象-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
....添加如下代码....
<property name="sessionManager" ref="sessionManager"/>
</bean>

解决Redis实现session共享存在的问题:
多次访问redis;
CustomSessionManager.java中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class CustomSessionManager extends DefaultWebSessionManager {
/**
* 解决多次访问redis
* @param sessionKey
* @return
* @throws UnknownSessionException
*/
@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
Serializable sessionId = getSessionId(sessionKey);
ServletRequest request = null;
if (sessionKey instanceof WebSessionKey) {
request = ((WebSessionKey) sessionKey).getServletRequest();
}
if (request != null && sessionId != null) {
Session session = (Session) request.getAttribute(sessionId.toString());
if (session != null){
return session;
}
}
Session session = super.retrieveSession(sessionKey);
if (request != null && sessionId != null) {
request.setAttribute(sessionId.toString(), session);
}
return session;
}
}

Shiro使用Redis实现缓存管理

  1. CacheManeger、Cache
    可以使用echache或者shiro实现。
  2. Redis实现CacheManager

添加RedisCacheManager.java:

1
2
3
4
5
6
7
8
9
public class RedisCacheManager implements CacheManager {

@Resource
private RedisCache redisCache;
@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
return redisCache;
}
}

添加RedisCache.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@Component
public class RedisCache<K, V> implements Cache<K, V> {

@Resource
private JedisUtil jedisUtil;

private final String CACHE_PREFIX = "lucifer-cache:";

private byte[] getKey(K k) {
if (k instanceof String) {
return (CACHE_PREFIX + k).getBytes();
}

return SerializationUtils.serialize(k);
}

@Override
public V get(K k) throws CacheException {
System.out.println("从redis中获取数据");
byte[] value = jedisUtil.get(getKey(k));
if (value != null) {
return (V) SerializationUtils.deserialize(value);
}
return null;
}

@Override
public V put(K k, V v) throws CacheException {
byte[] key = getKey(k);
byte[] value = SerializationUtils.serialize(v);
jedisUtil.set(key, value);
jedisUtil.expire(key, 600);
return v;
}

@Override
public V remove(K k) throws CacheException {
byte[] key = getKey(k);
byte[] value = jedisUtil.get(key);
jedisUtil.del(key);
if (value != null) {
return (V) SerializationUtils.deserialize(value);
}
return null;
}

@Override
public void clear() throws CacheException {
//
}

@Override
public int size() {
return 0;
}

@Override
public Set<K> keys() {
return null;
}

@Override
public Collection<V> values() {
return null;
}
}

spring.xml中添加配置

1
2
3
4
5
6
7
<bean class="com.lucifer.cache.RedisCacheManager" id="cacheManager"/>

<!--创建securityManager对象-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
....省略....
<property name="cacheManager" ref="cacheManager"/>
</bean>

Shiro自动登录

  1. Shiro RememberMe

spring.xml中添加

1
2
3
4
5
6
7
<bean class="org.apache.shiro.web.mgt.CookieRememberMeManager" id="cookieRememberMeManager">
<property name="cookie" ref="cookie"/>
</bean>
<bean class="org.apache.shiro.web.servlet.SimpleCookie" id="cookie">
<constructor-arg value="rememberMe"/>
<property name="maxAge" value="200000"/>
</bean>

controller中的示例代码:

1
2
3
4
5
6
7
@RequestMapping(value = "/subLogin", method = RequestMethod.POST, produces = "application/json;charset=utf-8")
@ResponseBody
public String subLogin(User user) {
........
token.setRememberMe(user.isRememberMe());
........
}

遇到问题

问题一

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘securityManager’ defined in class path resource [spring/spring-shiro.xml]: Cannot resolve reference to bean ‘shiroCacheManager’ while setting bean property ‘cacheManager’; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘shiroCacheManager’ defined in class path resource [spring/spring-shiro.xml]: Initialization of bean failed; nested exception is org.springframework.beans.ConversionNotSupportedException: Failed to convert property value of type [org.springframework.cache.ehcache.EhCacheCacheManager] to required type [net.sf.ehcache.CacheManager] for property ‘cacheManager’; nested exception is java.lang.IllegalStateException: Cannot convert value of type [org.springframework.cache.ehcache.EhCacheCacheManager] to required type [net.sf.ehcache.CacheManager] for property ‘cacheManager’: no matching editors or conversion strategy found

处理方式:
spring-ehcache.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--Ehcache配置-->
<!-- 启用缓存 -->
<cache:annotation-driven cache-manager="ehCacheManager"/>
<!-- 声明缓存管理器 -->
<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:ehcache.xml" />
<!-- true:一个cacheManager对象共享,false:多个对象独立 -->
<property name="shared" value="true"/> <!-- 这里是关键!!!没有必错 -->
</bean>

<bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManager" ref="ehcache"/>
</bean>

修改为

1
2
3
4
5
6
<!-- 声明缓存管理器 -->
<bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:ehcache.xml" />
<!-- true:一个cacheManager对象共享,false:多个对象独立 -->
<property name="shared" value="true"/> <!-- 这里是关键!!!没有必错 -->
</bean>

spring-shiro.xml

1
2
3
4
5
6
7
8
9
10
<!-- 缓存管理器 使用Ehcache实现 -->
<bean id="shiroCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManager" ref="ehCacheManager" />
</bean>

<!-- 权限管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm"/>
<property name="cacheManager" ref="shiroCacheManager"/>
</bean>

问题二

2019-04-11 23:16:31,395 ERROR [RMI TCP Connection(4)-127.0.0.1] org.springframework.web.context.ContextLoader - Context initialization failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘shiroFilter’ defined in class path resource [spring/spring-shiro.xml]: Cannot resolve reference to bean ‘formAuthenticationFilter’ while setting bean property ‘filters’ with key [TypedStringValue: value [authc], target type [null]]; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘formAuthenticationFilter’ defined in class path resource [spring/spring-shiro.xml]: Error setting property values; nested exception is org.springframework.beans.NotWritablePropertyException: Invalid property ‘loginUrl’ of bean class [com.weyoung.platform.shiro.filter.FormAuthenticationFilter]: Bean property ‘loginUrl’ is not writable or has an invalid setter method. Does the parameter type of the setter match the return type of the getter?

相关