Spring Security架设,路径级和方法级安全,以及403、404错误处理
本文写于2018-10-6,相关内容可能已经过期
依赖如下,其他版本不保证成功
Maven: org.springframework.security:spring-security-config:4.2.6.RELEASE
Maven: org.springframework.security:spring-security-core:4.2.6.RELEASE
Maven: org.springframework.security:spring-security-web:4.2.6.RELEASE
书接上回,既然开始使用Spring框架,自然就要自己完整写个小项目折腾一下了。于是在工作时间之外,自己建了一个很小的项目,算是比较完整地实现了一个web的功能,(很丑的)前端、controller、serverice、持久层和权限控制,其他的都不难,因为反正老夫写代码……就是复制粘贴……能跑起来就行。

但在Spring sceurity配置过程中踩了很多坑,折腾了一下午加一晚上,主要是很多暗坑猝不及防,应该记录一下。网上的资料五花八门,也有不少已经过时了,也踩了不少坑,这里就基于我配置成功的经历,在这里做一记录。
参考资料
Spring Security系列二 用户登录认证数据库实现
建议看完我这一篇文章再回去看这些资料哦,条理可能更清晰一些。
准备知识
摘自《Spring实战(第四版)》,人民邮电出版社。
Spring Security充分利用了依赖注入(DI)和面向切面(AOP)技术,因此能够在web请求级别和方法调用级别处理身份认证和授权。这也就是题目中的路径级和方法级安全,再解释清楚一点,一个是当你访问特定路径时予以权限认证,比如/admin只允许特权用户访问,而/user允许所有认证用户访问,/login和/以及/index允许所有人访问,一个是在你使用特定controller方法时予以权限认证。
Spring Security其实是面向切面技术的典型应用,但我们并不用像普通的AOP那样自己去写相应的filter和interceptor,因为框架已经帮我们完成了大部分工作,我们只需要辅助框架把剩下的工作完成就可以了。
要实现一个完整的权限认证,包含以下几个部分:
- 继承并实现自己的
WebSecurityConfigurerAdapter- 覆写
authenticationProvider(); - 覆写
configure()(共三个重载版本,可以只覆写其中一两个)
- 覆写
- 实现自己的用户信息查询服务,包括:
- 基于内存的用户查询服务;
- 基于持久层的用户查询服务。
- 提供前端login界面。(据说如不提供会使用一个默认的login页面)
- 如需要,实现路径级和方法级安全。
以下分别介绍之。
继承WebSecurityConfigurerAdapter,实现权限控制
最简单的权限控制配置如下:
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity //启用web安全性
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
此时就实现了权限控制。当你再次尝试登陆web系统时,你就会发现,没有人能够进入该系统。这是因为,该类中有三个与web安全相关的重要方法
configure(AuthenticationManagerBuilder)
configure(HttpSecurity)
configure(WebSecurity)
特别是configure(HttpSecurity http)只有默认实现,而他的默认实现如下:
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
其中.authorizeRequests().anyRequest().authenticated()是指所有进入应用的HTTP请求都必须进行验证,而此时并没有重载configure(AuthenticationManagerBuilder)方法,也就是说没有提供用户数据支撑,没有用户数据就相当于没有用户,因此所有的请求都需要认证,但没有人能认证成功。
但至少这一步,我们已经实现了权限控制,只是因为没有用户,所有的访问都被拒绝了,接下来就是添加用户了。
坑:这里还需要注意一点,就是如果使用这个默认的HttpSecurity时,会有一个默认的CSRF的要求,即跨域访问的token要求,如果在前端页面上没有提供的话,就会导致无法通过Spring Security验证,得到一个403问题。这个问题的具体解释见spring-security中的csrf防御机制。解决方法是要么在前端提供csrf的token,要么在此处建用掉csrf的验证。我目前先禁用了,但对于一个折腾党,我显然以后会提供这个token:
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() //禁用csrf
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
提供自己的用户信息查询服务
书上说,Spring Security非常灵活,支持各种数据存储来认证用户,内置了多种常见的用户存储场景,比如内存存储、关系数据库以及LDAP,而且我们也可以编写并插入自定义的用户存储实现,在这里我主要是用了关系数据库(实际是自己实现的用户存储实现),但介绍基于内存的用户存储实际上挺有必要。
因为我们已经实现了WebSecurityConfigurerAdapter,所以配置用户存储最简单的方式就是重载configure()方法,并以AuthenticationManagerBuilder为传入参数,无论是内存存储还是数据库存储还是其他方式,我们都可以借此实现。
基于内存的用户存储
如下配置:
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity //启用web安全性
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("user123").roles("user")
.and()
.withUser("admin").password("admin123").roles("admin","user");
}
}
我们此时添加了两个用户。要注意的是,roles方法实际上是authorities()方法的一种简写形式,roles的所有值都会被自动添加一个”ROLE_”前缀,并将其作为权限授予给用户。实际上,以上用户配置与下面的等同:
auth.inMemoryAuthentication()
.withUser("user").password("user123").authorities("ROLE_user")
.and()
.withUser("admin").password("admin123").authorities("ROLE_admin","ROLE_user");
这里就隐藏了一个小坑,我们先记着这一块的前缀,一会再回来继续研究它。
这样再次运行,就可以实现登陆。登陆后会跳转到原来的请求的url,比如当你申请/时,被重定向到/login,那么登陆后会再次跳转到/去。
这种基于内存的登陆模型对于特别简单的系统的权限控制(开发了一个小网站只给家人存照片用)是不错的,也对单元测试人员很友好,但在生产环境中显然受到很大限制,因此最好迁移到关系数据库中去。
基于关系数据库进行认证
如下配置:
@Autowired
DataSource dataSource
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource);
}
}
这样就可以了。因为神奇的(实际上我感觉对于我这样的新手来说是学习曲线比较陡的)约定大于配置,我们这个dataSource的数据库模式是有一定要求的,因为当查找数据库时,Spring Security内部(org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl)会使用以下默认配置:
public static final java.lang.String DEF_USERS_BY_USERNAME_QUERY=
"select username,password,enabled from users where username = ?";
public static final java.lang.String DEF_AUTHORITIES_BY_USERNAME_QUERY=
"select username,authority from authorities where username = ?";
public static final java.lang.String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY=
"select g.id, g.group_name, ga.authority from groups g, group_members gm," +
"group_authorities ga where gm.username = ? " +
"and g.id = ga.group_id and g.id = gm.group_id";
第一个查询中获取了用户名、密码及用户是否启用的信息,这些信息会用进行用户认证。接下来的查询查找了用户所授予的权限,(authority,请联想上面的.withUser("user").authorities("ROLE_user")),在接下来的路径级、方法级安全中会用到这些权限,最后一个查询中,查找了用户作为群组成员所授予的权限,但如果不实现其实(数据表为空)也没关系,只要第二个表实现了就行。
如果你按照约定,这样设计自己的数据库,那么auth.jdbcAuthentication().dataSource(dataSource)这样的极简配置就适用于你。但是有时候我们的表结构与上面的并不一致,这可以通过进一步的配置来覆盖约定。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery
("select username,password,true from table_name where username = ?")
.authoritiesByUsernameQuery
("select username, 'ROLE_admin' from table_name where username = ?");
}
}
简单易懂,完全不用解释,这样的代码才是优雅的代码。写一大堆注释才能让人看懂的都是垃圾代码……就像我写的那些代码……?
自己实现的用户身份认证
我呢,用的是关系数据库,但是又用的是网络教程中比较流行的双table自定义用户身份认证。具体地讲,就是通过实现自己的UserDetails和UserDetailsService类,再在configure里注册自己的UserDetailsService,用这种方式实现,configure的配置如下
@Bean
public UserDetailsService userDetailsService() {
return new CustomUserService();
}
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
}
其中CustomUserService就是自己实现的用户信息提供类,具体实现如下:
import com.mystory.twitter.model.SysUser;
import com.mystory.twitter.repository.SysUserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
public class CustomUserService implements UserDetailsService {
@Autowired
SysUserRepository sysUserRepository;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
SysUser user = sysUserRepository.findByUsername(s);
if (user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
return user;
}
}
其中SysUser的定义如下:
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Entity
@Getter //使用lombok的注解,为类提供getter和setter方法。
@Setter
public class SysUser implements UserDetails {
@Id
@GeneratedValue
private Integer id;
private String username;
private String password;
private String roles;
@ManyToMany(cascade = {CascadeType.REFRESH},fetch = FetchType.EAGER)
//ManyToMany注解又新建了一张表。用来存UserRole和SysUser的映射关系
private List<UserRole> roles; //这是另外一个专门存储role的表
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> auths = new ArrayList<>();
List<UserRole> roles = this.getRoles();
for (UserRole role : roles) {
auths.add(new SimpleGrantedAuthority(role.getRole()));
}
return auths;
}
//接下来是一些覆写后的样板代码,都有各自的实际意义,可以实现更精确的用户控制。此处全置true
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
而另外一个UserRole表的实现如下:
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Getter
@Setter
public class UserRole {
@Id
@GeneratedValue
private Integer id;
private String username;
private String role;
}
SysUserRepository的定义如下,实际上是一个接口,由Spring框架自动实现并实例化。
import com.mystory.twitter.model.SysUser;
import org.springframework.data.repository.CrudRepository;
import java.util.List;
public interface SysUserRepository extends CrudRepository<SysUser,String> {
List<SysUser> findAll();
SysUser findByUsername(String username);
}
至此,我们得到了以下三张表:
+-------------------+
| Tables_in_tbname |
+-------------------+
| user_role |
| sys_user |
| sys_user_roles |
+-------------------+
其结构分别是:
mysql> desc sys_user;
+----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| password | varchar(255) | YES | | NULL | |
| username | varchar(255) | YES | | NULL | |
+----------+--------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)
mysql> desc user_role;
+----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| role | varchar(255) | YES | | NULL | |
| username | varchar(255) | YES | | NULL | |
+----------+--------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)
mysql> desc sys_user_roles;
+-------------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------------+---------+------+-----+---------+-------+
| sys_user_id | int(11) | NO | MUL | NULL | |
| roles_id | int(11) | NO | MUL | NULL | |
+-------------+---------+------+-----+---------+-------+
此时我们往表里插入一点数据,比如:
INSERT INTO sys_user (id, password, username)
VALUES
(1, 'adminpasswd', 'admin'),
(2, 'userpasswd' , 'user');
INSERT INTO user_role (id, role, username)
VALUES
(1, 'ROLE_admin', 'admin'),
(2, 'ROLE_user' , 'user');
INSERT INTO sys_user_roles (sys_user_id, roles_id)
VALUES
(1, 1),
(2, 2);
要注意的是,sys_user_role的数据,必须找到正确的对应关系,使得username和相关权限是正确对应的。
坑:注意user_role表中的role,在数据库中必须写成ROLE_具体权限,比如ROLE_admin、ROLE_user等,才能在之后搭配路径和方法安全性的hasRole方法使用,否则将会得到一个403错误,意思是权限不通过。如:

关键是错误信息还是三个问号……没有任何提示作用。
提供自己的login界面
如果不提供login界面,Spring Security会提供一个默认的login界面,我因为最开始一直就有login界面,没有见过这个传说中的默认界面,但在提供login界面时,也有一个约定,那就是必须用username传输用户名,password传输密码,也就是说,前端的form必须是这样的:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<title>一个登陆页面</title>
</head>
<body>
<h1 align="center">某个系统的登陆界面</h1>
<form th:action="@{/login}" method="post">
<div align="center">
<span>用户名:</span>
<!--用户名必须用username这个name,才能被spring security正确接收-->
<input name="username" type="text" placeholder="用户名"/>
</div>
<br/>
<div align="center">
<span>密 码:</span>
<!--密码必须用password这个name,才能被spring security正确接收-->
<input name="password" type="password" placeholder="密码"/>
</div>
<br/>
<div align="center">
<button type="submit" style="width:190px;">登 录</button>
</div>
</form>
</body>
</html>
坑:改掉name会导致Spring Security接收不到值,但这里并不会抛异常,只会默默地用空值去验证,也就是绝对验证不过,我觉得这样的实现真是不友好啊,应该抛个异常啥的,毕竟用空用户名去匹配显然不会是我们希望的行为呀。
实现路径级和方法级权限控制
通过以上方法为WebSecurityConfigurerAdapter提供用户信息后,就可以正常登陆了,但此时还有一个问题,那就是所有人的权限都是一样的,不能实现诸如/admin路径下的资源只能由admin用户访问这样的权限控制。
实现这样精细的权限控制有两种方式,一个是在WebSecurityConfigurerAdapter的configure(HttpSecurity http)中进行配置,比如要求/admin只能由role包含admin的用户访问,即路径级权限控制:
路径级权限控制
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/admin/**").hasAnyRole("admin")
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error")
.defaultSuccessUrl("/funcs", true)
.permitAll()
.and()
.logout()
.permitAll();
}
这里要注意,.authorizeRequests().antMatchers("/admin/**").hasAnyRole("admin")要放在前面,如果放在后面.defaultSuccessUrl("/funcs", true).permitAll(),就有可能出现所有权限请求都批准,user也能访问/admin的情况,具体原因不明。
这就实现了路经级的安全控制,非admin用户访问/admin/**会得到一个403。
看到这里,我们可以回头再去讨论数据库中那个”ROLE_admin”和”ROLE_user”的问题了。Spring这里又来了一个暗搓搓的约定,也就是说Rols表里,数据库存role时必须带有ROLE_前缀,但是在程序中写hasRole或者hasAnyRole这种权限控制时,就不能带这个前缀(带的话甚至会抛异常直接退出程序),于是像我这样的初学者,就绕在这个权限里了,我一心想着数据库里的role和这里的hasRole要一致,谁能想到程序会暗搓搓地加一个ROLE_呢?这种莫名其妙的约定,经简单的考据,只是因为早期的Security要求在数据库和程序代码中都带有ROLE_前缀,后来觉得在程序里这样太冗余,于是改为程序自动加一个前缀,但数据库中的前缀仍然不能省。为解决这个问题查了几个小时啊……我觉得我一辈子在代码中加ROLE_前缀可能耗费的时间都到不了几个小时。因此我觉得有些莫名其妙的不一致,实在是很坑爹。
方法级权限控制
但Spring官方并不是非常推荐路径级的权限控制,他们更推荐方法级的权限控制。因为这样更直观,而且粒度更精细一点。要开启方法级的权限控制,需要做这么几件事。
- 在
WebSecurityConfigurerAdapter上加注解,开启方法级权限控制。@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) //重要的是这个注解 public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { ... } @Override protected void configure(HttpSecurity http) throws Exception { ... } }@EnableGlobalMethodSecurity还有一些其他更详细的配置,因为我这里还没有用,因此没有仔细了解,有兴趣的可以了解一下,我这里只用了
prePostEnabled = true这一个参数,意思是在方法调用之前,基于表达式的计算结果来限制对方法的访问,也就是现在Spring Security框架将开始解析controller方法上的@PreAuthorize("hasRole('admin')")这样的注解,并以此来控制权限了。 -
在具体方法上添加权限控制注解:
@RestController @RequestMapping("/admin") public class AdminController { @GetMapping("/adminfunc1") @PreAuthorize("hasRole('admin')") public ModelAndView adminFunc1Page(ModelAndView modelAndView){ //do sth here return modelAndView; } @PostMapping("/adminfunc1") @PreAuthorize("hasRole('admin')") public ModelAndView adminFunc1(ModelAndView modelAndView){ //do sth here return modelAndView; } }这样就可以保证
/admin/adminfunc1只允许admin访问,其他role的用户将得到一个404。可以看到,这里实现了更为精确的控制,Post和Get方法都可以允许不同的用户访问,其实还是不错的。@PreAuthorize的属性可以有以下几种形式:@PreAuthorize("hasRole('admin')"),@PreAuthorize("hasAnyRole('admin', 'user')")或者@PreAuthorize("hasRole('admin') and hasRole('user')"),或者@PreAuthorize("hasRole('admin') or hasRole('user')"),意思十分清楚。
在同一个页面区分不同的role,展示不同的内容
比如说我们有个功能菜单页,我希望普通用户只能看到一部分功能,而admin用户可以看到更高级的功能,但又不想做两个页面,怎么办呢?其实这个问题翻译一下,就是如何在controller方法中查看用户身份,这个可以通过安全上下文来获取,具体如下:
@GetMapping("/func")
@PreAuthorize("hasAnyRole('admin','user')")
public ModelAndView showFunc(ModelAndView modelAndView, HttpSession session) {
SecurityContext ctx = SecurityContextHolder.getContext();
Authentication auth = ctx.getAuthentication();
SysUser user = (SysUser) auth.getPrincipal();
//既然已经获取了user,SysUser中就有一个List<UserRole>字段,当然可以用来判断用户身份了。
}
对密码进行加密
现在我们获得了一个功能基本完备的系统,但是还有个小问题,因为在数据库中采取了明文方式存储密码,显然不安全,有必要对密码进行加密存储。这就涉及到Spring Security的另外一个功能模块:PasswordEncoder
这一块Spring Security系列三 用户密码加密实现已经解释地很清楚了,有新老两款PasswordEncoder,旧的因为存在使用不方便、存在安全隐患等问题,已经被废弃了,现在推荐使用新的PasswordEncoder,因此旧的也就不在这里介绍了。
要使用PasswordEncoder,需要对WebSecurityConfigurerAdapter进行比较大的改造,具体来讲,就是用AuthenticationProvider来提供相关权限控制,代码胜千言,完整代码如下:
import com.mystory.twitter.Engine.CustomUserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean //不能省略@bean注解
public UserDetailsService userDetailsService() {
return new CustomUserService();
}
@Bean //不能省略@bean注解
public PasswordEncoder passwordEncoder() {
//Spring官方推荐新项目使用这个PasswordEncoder
return new BCryptPasswordEncoder();
}
@Bean //不能省略@bean注解
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider =
new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService());
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
//原来的auth逻辑都改为在authenticationProvider()中实现。
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
//.authorizeRequests() //实现了方法级权限控制,因此这一段去掉了
//.antMatchers("/root/**")
//.hasAnyRole("admin")
//.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error")
.defaultSuccessUrl("/func", true)
.permitAll()
.and()
.logout()
.permitAll();
}
}
此时要注意的是,数据库中的password字段也要进行修改,即使用:String password = passwordEncoder().encode(plainPassword);将明文加密后,放到数据库里面去即可。
两个意料之外的403和一个404错误
两个403前面都介绍过了,一个是因为csrf功能未禁用导致的,一个是数据库中的role没有添加ROLE_前缀导致无法匹配导致的。详情可见之前。
一个404的原因是这样的, 正如我们上文说道,当登陆成功后,将会返回登陆前的界面,但假如这个界面没有实现,(比如奇葩如我,/没有实现,我的本意是直接定位到/login,login完了就跳功能选择界面了,但一般访问都是直接访问到/,于是登陆后就回跳就成了404了),为解决这个问题,可以设置.defaultSuccessUrl("/func", true),就在protected void configure(HttpSecurity http)方法中,打开后面的true后,即可解决跳转后404问题。但这个功能要不要打开是个问题,因为其实大多数情况下,我们需要的就是跳回登录前界面。需要自己斟酌。
0 条评论