Spring Security架设,路径级和方法级安全,以及403、404错误处理

于由astupidcoder发布

本文写于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 Security系列二 用户登录认证数据库实现

Spring Security系列三 用户密码加密实现

建议看完我这一篇文章再回去看这些资料哦,条理可能更清晰一些。

准备知识

摘自《Spring实战(第四版)》,人民邮电出版社。

Spring Security充分利用了依赖注入(DI)和面向切面(AOP)技术,因此能够在web请求级别和方法调用级别处理身份认证和授权。这也就是题目中的路径级和方法级安全,再解释清楚一点,一个是当你访问特定路径时予以权限认证,比如/admin只允许特权用户访问,而/user允许所有认证用户访问,/login和/以及/index允许所有人访问,一个是在你使用特定controller方法时予以权限认证。

Spring Security其实是面向切面技术的典型应用,但我们并不用像普通的AOP那样自己去写相应的filter和interceptor,因为框架已经帮我们完成了大部分工作,我们只需要辅助框架把剩下的工作完成就可以了。

要实现一个完整的权限认证,包含以下几个部分:

  1. 继承并实现自己的WebSecurityConfigurerAdapter
    1. 覆写authenticationProvider();
    2. 覆写configure()(共三个重载版本,可以只覆写其中一两个)
  2. 实现自己的用户信息查询服务,包括:
    1. 基于内存的用户查询服务;
    2. 基于持久层的用户查询服务。
  3. 提供前端login界面。(据说如不提供会使用一个默认的login页面)

  4. 如需要,实现路径级和方法级安全。

以下分别介绍之。

继承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错误,意思是权限不通过。如:

Spring-security-403.png

关键是错误信息还是三个问号……没有任何提示作用。

提供自己的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官方并不是非常推荐路径级的权限控制,他们更推荐方法级的权限控制。因为这样更直观,而且粒度更精细一点。要开启方法级的权限控制,需要做这么几件事。

  1. 在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')")这样的注解,并以此来控制权限了。

  2. 在具体方法上添加权限控制注解:

    @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 条评论

发表回复

Avatar placeholder

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据。