SpringSecurity开搞

SpringSecurity开搞

1.两大核心功能

  • 认证

    HTTP BASIC authentication headers:基于IETF RFC 标准。

    HTTP Digest authentication headers:基于IETF RFC 标准。

    HTTP X.509 client certificate exchange:基于IETF RFC 标准。

    LDAP:跨平台身份验证。

    Form-based authentication:基于表单的身份验证。

    Run-as authentication:用户用户临时以某一个身份登录。

    OpenID authentication:去中心化认证。

    Jasig Central Authentication Service:单点登录。

    Automatic “remember-me” authentication:记住我登录(允许一些非敏感操作)。

  • 授权

2.自定义用户名,密码

  • 配置文件中设置

    spring.security.user.name=njuptwly
    spring.security.user.password=123456
    
  • 配置类

    指定 PasswordEncoder

    Spring Security 提供了多种密码加密方案,官方推荐使用 BCryptPasswordEncoder,BCryptPasswordEncoder 使用 BCrypt 强哈希函数,开发者在使用时可以选择提供 strength 和 SecureRandom 实例。strength 越大,密钥的迭代次数越多,密钥迭代次数为 2^strength。strength 取值在 4~31 之间,默认为 10。

    不同于 Shiro 中需要自己处理密码加盐,在 Spring Security 中,BCryptPasswordEncoder 就自带了盐,处理起来非常方便。

    而 BCryptPasswordEncoder 就是 PasswordEncoder 接口的实现类。

    PasswordEncoder 接口

    public interface PasswordEncoder {
    	String encode(CharSequence rawPassword);
    	boolean matches(CharSequence rawPassword, String encodedPassword);
    	default boolean upgradeEncoding(String encodedPassword) {
    		return false;
    	}
    }
    
    1. encode 方法用来对明文密码进行加密,返回加密之后的密文。
    2. matches 方法是一个密码校对方法,在用户登录的时候,将用户传来的明文密码和数据库中保存的密文密码作为参数,传入到这个方法中去,根据返回的 Boolean 值判断用户密码是否输入正确。
    3. upgradeEncoding 是否还要进行再次加密,这个一般来说就不用了。

    正式配置

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
      
        @Bean
        PasswordEncoder passwordEncoder() {
            return NoOpPasswordEncoder.getInstance();
        }
      
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                    .withUser("njuptwly")
                    .password("123456").roles("admin");
        }
    }
    
    1. 首先我们自定义 SecurityConfig 继承自 WebSecurityConfigurerAdapter,重写里边的 configure 方法。
    2. 首先我们提供了一个 PasswordEncoder 的实例,因为目前案例比较简单,因此暂时先不给密码进行加密,所以返回 NoOpPasswordEncoder 的实例即可。
    3. configure 方法中,我们通过 inMemoryAuthentication 来开启在内存中定义用户,withUser 中是用户名,password 中则是用户密码,roles 中是用户角色。
    4. 如果需要配置多个用户,用 and 相连。

    为什么用 and 相连?

    在没有 Spring Boot 的时候,我们都是 SSM 中使用 Spring Security,这种时候都是在 XML 文件中配置 Spring Security,既然是 XML 文件,标签就有开始有结束,现在的 and 符号相当于就是 XML 标签的结束符,表示结束当前标签,这是个时候上下文会回到 inMemoryAuthentication 方法中,然后开启新用户的配置。

    3.服务端定义

    继续重写config的 configure(WebSecurity web)configure(HttpSecurity http) 方法,如下:

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/js/**", "/css/**","/images/**");
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")
                .permitAll()
                .and()
                .csrf().disable();
    }
    
    1. web.ignoring() 用来配置忽略掉的 URL 地址,一般对于静态文件,我们可以采用此操作。
    2. 如果我们使用 XML 来配置 Spring Security ,里边会有一个重要的标签 <http>,HttpSecurity 提供的配置方法 都对应了该标签。(因此,以and连接)
    3. authorizeRequests 对应了 <intercept-url>
    4. formLogin 对应了 <formlogin>。默认登录页面地址和登录接口地址相同,一个为get,一个为post
    5. and 方法表示结束当前标签,上下文回到HttpSecurity,开启新一轮的配置。
    6. permitAll 表示登录相关的页面/接口不要被拦截。
    7. 最后记得关闭 csrf ,之后聊csrf问题

    登录页面地址和登录接口地址分离

    .and()
    .formLogin()
    .loginPage("/login.html")
    .loginProcessingUrl("/doLogin")
    .permitAll()
    .and()
    

    再修改前端表单

    登录页面里边的 action 属性,改为 /doLogin,如下:
    <form action="/doLogin" method="post">
    <!--省略-->
    </form>
    

    4.登陆成功回调

    1. defaultSuccessUrl 有一个重载的方法,我们先说一个参数的 defaultSuccessUrl 方法。如果我们在 defaultSuccessUrl 中指定登录成功的跳转页面为 /index,此时分两种情况,如果你是直接在浏览器中输入的登录地址,登录成功后,就直接跳转到 /index,如果你是在浏览器中输入了其他地址,例如 http://localhost:8080/hello,结果因为没有登录,又重定向到登录页面,此时登录成功后,就不会来到 /index ,而是来到 /hello 页面。
    2. defaultSuccessUrl 还有一个重载的方法,第二个参数如果不设置默认为 false,也就是我们上面的的情况,如果手动设置第二个参数为 true,则 defaultSuccessUrl 的效果和 successForwardUrl 一致。
    3. successForwardUrl 表示不管你是从哪里来的,登录后一律跳转到 successForwardUrl 指定的地址。例如 successForwardUrl 指定的地址为 /index ,你在浏览器地址栏输入 http://localhost:8080/hello,结果因为没有登录,重定向到登录页面,当你登录成功之后,就会服务端跳转到 /index 页面;或者你直接就在浏览器输入了登录页面地址,登录成功后也是来到 /index
    .and()
    .formLogin()
    .loginPage("/login.html")
    .loginProcessingUrl("/doLogin")
    .usernameParameter("name")
    .passwordParameter("passwd")
    .defaultSuccessUrl("/index")
    .successForwardUrl("/index")
    .permitAll()
    .and()
    

    实际操作中,defaultSuccessUrl 和 successForwardUrl 只需要配置一个即可。

    5.登陆失败回调

    • failureForwardUrl
    • failureUrl

    6.注销登录

    .and()
    .logout()
    .logoutUrl("/logout")
    .logoutRequestMatcher(new AntPathRequestMatcher("/logout","POST"))
    .logoutSuccessUrl("/index")
    .deleteCookies()
    .clearAuthentication(true)
    .invalidateHttpSession(true)
    .permitAll()
    .and()
    
    1. 默认注销的 URL 是 /logout,是一个 GET 请求,我们可以通过 logoutUrl 方法来修改默认的注销 URL。
    2. logoutRequestMatcher 方法不仅可以修改注销 URL,还可以修改请求方式,实际项目中,这个方法和 logoutUrl 任意设置一个即可。
    3. logoutSuccessUrl 表示注销成功后要跳转的页面。
    4. deleteCookies 用来清除 cookie。
    5. clearAuthentication 和 invalidateHttpSession 分别表示清除认证信息和使 HttpSession 失效,默认可以不用配置,默认就会清除。

    7.前后端分离

    有状态交互与无状态交互

    有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如 Tomcat 中的 Session。例如登录:用户登录后,我们把用户的信息保存在服务端 session 中,并且给用户一个 cookie 值,记录对应的 session,然后下次请求,用户携带 cookie 值来(这一步有浏览器自动完成),我们就能识别到对应 session,从而找到用户的信息。这种方式目前来看最方便,但是也有一些缺陷,如下:

    • 服务端保存大量数据,增加服务端压力
    • 服务端保存用户状态,不支持集群化部署
    什么是无状态

    微服务集群中的每个服务,对外提供的都使用 RESTful 风格的接口。而 RESTful 风格的一个最重要的规范就是:服务的无状态性,即:

    • 服务端不保存任何客户端请求者信息
    • 客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份

    那么这种无状态性有哪些好处呢?

    • 客户端请求不依赖服务端的信息,多次请求不需要必须访问到同一台服务器
    • 服务端的集群和状态对客户端透明
    • 服务端可以任意的迁移和伸缩(可以方便的进行集群化部署)
    • 减小服务端存储压力
    如何实现无状态

    无状态登录的流程:

    • 首先客户端发送账户名/密码到服务端进行认证
    • 认证通过后,服务端将用户信息加密并且编码成一个 token,返回给客户端
    • 以后客户端每次发送请求,都需要携带认证的 token
    • 服务端对客户端发送来的 token 进行解密,判断是否有效,并且获取用户登录信息
    各自优缺点

    使用 session 最大的优点在于方便。你不用做过多的处理,一切都是默认的即可。

    但是使用 session 有另外一个致命的问题就是如果你的前端是 Android、iOS、小程序等,这些 App 天然的就没有 cookie,如果非要用 session,就需要这些工程师在各自的设备上做适配,一般是模拟 cookie,从这个角度来说,在移动 App 遍地开花的今天,我们单纯的依赖 session 来做安全管理,似乎也不是特别理想。

    这个时候 JWT 这样的无状态登录就展示出自己的优势了,这些登录方式所依赖的 token 你可以通过普通参数传递,也可以通过请求头传递,怎么样都行,具有很强的灵活性。

    登陆成功

    .successHandler((req, resp, authentication) -> {
        Object principal = authentication.getPrincipal();
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.write(new ObjectMapper().writeValueAsString(principal));
        out.flush();
        out.close();
    })
    

    successHandler 方法的参数是一个 AuthenticationSuccessHandler 对象,这个对象中我们要实现的方法是 onAuthenticationSuccess。

    onAuthenticationSuccess 方法有三个参数,分别是:

    • HttpServletRequest
    • HttpServletResponse
    • Authentication

    有了前两个参数,我们就可以在这里随心所欲的返回数据了。利用 HttpServletRequest 我们可以做服务端跳转,利用 HttpServletResponse 我们可以做客户端跳转,当然,也可以返回 JSON 数据。

    第三个 Authentication 参数则保存了我们刚刚登录成功的用户信息。

    登陆失败

    .failureHandler((req, resp, e) -> {
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.write(e.getMessage());
        out.flush();
        out.close();
    })
    
    resp.setContentType("application/json;charset=utf-8");
    PrintWriter out = resp.getWriter();
    RespBean respBean = RespBean.error(e.getMessage());
    if (e instanceof LockedException) {
        respBean.setMsg("账户被锁定,请联系管理员!");
    } elseif (e instanceof CredentialsExpiredException) {
        respBean.setMsg("密码过期,请联系管理员!");
    } elseif (e instanceof AccountExpiredException) {
        respBean.setMsg("账户过期,请联系管理员!");
    } elseif (e instanceof DisabledException) {
        respBean.setMsg("账户被禁用,请联系管理员!");
    } elseif (e instanceof BadCredentialsException) {
        respBean.setMsg("用户名或者密码输入错误,请重新输入!");
    }
    out.write(new ObjectMapper().writeValueAsString(respBean));
    out.flush();
    out.close();
    

    源码,登陆步骤(异常抛出)

    public Authentication authenticate(Authentication authentication)
    		throws AuthenticationException {
    	try {
    		user = retrieveUser(username,
    				(UsernamePasswordAuthenticationToken) authentication);
    	}
    	catch (UsernameNotFoundException notFound) {
    		logger.debug("User '" + username + "' not found");
    		if (hideUserNotFoundExceptions) {
    			thrownew BadCredentialsException(messages.getMessage(
    					"AbstractUserDetailsAuthenticationProvider.badCredentials",
    					"Bad credentials"));
    		}
    		else {
    			throw notFound;
    		}
    	}
    

    未认证处理方案

    Spring Security 中的一个接口 AuthenticationEntryPoint ,该接口有一个实现类:LoginUrlAuthenticationEntryPoint ,该类中有一个方法 commence,如下:

    默认源码

    public void commence(HttpServletRequest request, HttpServletResponse response,
    		AuthenticationException authException) {
    	String redirectUrl = null;
    	if (useForward) {
    		if (forceHttps && "http".equals(request.getScheme())) {
    			redirectUrl = buildHttpsRedirectUrlForRequest(request);
    		}
    		if (redirectUrl == null) {
    			String loginForm = determineUrlToUseForThisRequest(request, response,
    					authException);
    			if (logger.isDebugEnabled()) {
    				logger.debug("Server side forward to: " + loginForm);
    			}
    			RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
    			dispatcher.forward(request, response);
    			return;
    		}
    	}
    	else {
    		redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
    	}
    	redirectStrategy.sendRedirect(request, response, redirectUrl);
    }
    

    自定义写法

    .csrf().disable().exceptionHandling()
    .authenticationEntryPoint((req, resp, authException) -> {
                resp.setContentType("application/json;charset=utf-8");
                PrintWriter out = resp.getWriter();
                out.write("尚未登录,请先登录");
                out.flush();
                out.close();
            }
    );
    

    自定义写法

    .and()
    .logout()
    .logoutUrl("/logout")
    .logoutSuccessHandler((req, resp, authentication) -> {
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.write("注销成功");
        out.flush();
        out.close();
    })
    .permitAll()
    .and()
    

8.授权

http.authorizeRequests()
        .antMatchers("/admin/**").hasRole("admin")
        .antMatchers("/user/**").hasRole("user")
        .anyRequest().authenticated()
        .and()

运用了ant风格匹配符

通配符	  含义
**	    匹配多层路径
*	      匹配一层路径
?	      匹配任意单个字符

其中,anyRequest 一定要放在最后,否则抛出异常

源码如下

public abstract class AbstractRequestMatcherRegistry<C> {
 private boolean anyRequestConfigured = false;
 public C anyRequest() {
  Assert.state(!this.anyRequestConfigured, "Can't configure anyRequest after itself");
  this.anyRequestConfigured = true;
  return configurer;
 }
 public C antMatchers(HttpMethod method, String... antPatterns) {
  Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
  return chainRequestMatchers(RequestMatchers.antMatchers(method, antPatterns));
 }
 public C antMatchers(String... antPatterns) {
  Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
  return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns));
 }
 protected final List<MvcRequestMatcher> createMvcMatchers(HttpMethod method,
   String... mvcPatterns) {
  Assert.state(!this.anyRequestConfigured, "Can't configure mvcMatchers after anyRequest");
  return matchers;
 }
 public C regexMatchers(HttpMethod method, String... regexPatterns) {
  Assert.state(!this.anyRequestConfigured, "Can't configure regexMatchers after anyRequest");
  return chainRequestMatchers(RequestMatchers.regexMatchers(method, regexPatterns));
 }
 public C regexMatchers(String... regexPatterns) {
  Assert.state(!this.anyRequestConfigured, "Can't configure regexMatchers after anyRequest");
  return chainRequestMatchers(RequestMatchers.regexMatchers(regexPatterns));
 }
 public C requestMatchers(RequestMatcher... requestMatchers) {
  Assert.state(!this.anyRequestConfigured, "Can't configure requestMatchers after anyRequest");
  return chainRequestMatchers(Arrays.asList(requestMatchers));
 }
}

角色继承

上级可能具备下级的所有权限,如果使用角色继承,这个功能就很好实现,我们只需要在 SecurityConfig 中添加如下代码来配置角色继承关系即可:

@Bean
RoleHierarchy roleHierarchy() {
    RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
    hierarchy.setHierarchy("ROLE_admin > ROLE_user");
    return hierarchy;
}

注意,在配置时,需要给角色手动加上 ROLE_ 前缀。上面的配置表示 ROLE_admin 自动具备 ROLE_user 的权限。

9.用户登陆整合数据库

userservice

Spring Security 支持多种不同的数据源,这些不同的数据源最终都将被封装成 UserDetailsService 的实例,自己可以创建一个类实现 UserDetailsService 接口,我们也可以使用系统默认提供的 UserDetailsService 实例,例如前面提到的 InMemoryUserDetailsManager 。

JdbcUserDetailsManager 自己提供了一个数据库模型,这个数据库模型保存在如下位置:

org/springframework/security/core/userdetails/jdbc/users.ddl

这里存储的脚本内容如下:

create table users(username varchar_ignorecase(50) not null primary key,password varchar_ignorecase(500) not null,enabled boolean not null);
create table authorities (username varchar_ignorecase(50) not null,authority varchar_ignorecase(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);

可以看到,脚本中有一种数据类型 varchar_ignorecase,这个其实是针对 HSQLDB 数据库创建的,而我们使用的 MySQL 并不支持这种数据类型,所以这里需要大家手动调整一下数据类型,将 varchar_ignorecase 改为 varchar 即可。

修改完成后,创建数据库,执行完成后的脚本。

执行完 SQL 脚本后,我们可以看到一共创建了两张表:users 和 authorities。

  • users 表中保存用户的基本信息,包括用户名、用户密码以及账户是否可用。
  • authorities 中保存了用户的角色。
  • authorities 和 users 通过 username 关联起来。

配置完成后,接下来,我们将上篇文章中通过 InMemoryUserDetailsManager 提供的用户数据用 JdbcUserDetailsManager 代替掉,如下:

@Autowired
DataSource dataSource;

@Override
@Bean
protected UserDetailsService userDetailsService() {
    JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
    manager.setDataSource(dataSource);
    if (!manager.userExists("wly")) {
        manager.createUser(User.withUsername("wly").password("123").roles("admin").build());
    }
    if (!manager.userExists("njuptwly")) {
        manager.createUser(User.withUsername("njuptwly").password("123").roles("user").build());
    }
    return manager;
}
  1. 首先构建一个 JdbcUserDetailsManager 实例。
  2. 给 JdbcUserDetailsManager 实例添加一个 DataSource 对象。
  3. 调用 userExists 方法判断用户是否存在,如果不存在,就创建一个新的用户出来(因为每次项目启动时这段代码都会执行,所以加一个判断,避免重复创建用户)。
  4. 用户的创建方法和我们之前 InMemoryUserDetailsManager 中的创建方法基本一致。

这里的 createUser 或者 userExists 方法其实都是调用写好的 SQL 去判断的,我们从它的源码里就能看出来(部分):

public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsManager,
  GroupManager {
 public static final String DEF_USER_EXISTS_SQL = "select username from users where username = ?";

 private String userExistsSql = DEF_USER_EXISTS_SQL;

 public boolean userExists(String username) {
  List<String> users = getJdbcTemplate().queryForList(userExistsSql,
    new String[] { username }, String.class);

  if (users.size() > 1) {
   throw new IncorrectResultSizeDataAccessException(
     "More than one user found with name '" + username + "'", 1);
  }

  return users.size() == 1;
 }
}
数据库支持

通过前面的代码,大家看到这里需要数据库支持,所以我们在项目中添加如下两个依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

然后再在 application.properties 中配置一下数据库连接:

spring.datasource.username=root
spring.datasource.password=123
spring.datasource.url=jdbc:mysql:///security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

10.自定义用户登陆整合数据库

接下来我们创建两个实体类,分别表示用户角色了用户类:

用户角色:

@Entity(name = "t_role")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String nameZh;
    //省略 getter/setter
}

这个实体类用来描述用户角色信息,有角色 id、角色名称(英文、中文),@Entity 表示这是一个实体类,项目启动后,将会根据实体类的属性在数据库中自动创建一个角色表。

用户实体类:

@Entity(name = "t_user")
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;
    private boolean accountNonExpired;
    private boolean accountNonLocked;
    private boolean credentialsNonExpired;
    private boolean enabled;
    @ManyToMany(fetch = FetchType.EAGER,cascade = CascadeType.PERSIST)
    private List<Role> roles;
  
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : getRoles()) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }
    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
    //省略其他 get/set 方法
}

用户实体类主要需要实现 UserDetails 接口,并实现接口中的方法。

  1. accountNonExpired、accountNonLocked、credentialsNonExpired、enabled 这四个属性分别用来描述用户的状态,表示账户是否没有过期、账户是否没有被锁定、密码是否没有过期、以及账户是否可用。
  2. roles 属性表示用户的角色,User 和 Role 是多对多关系,用一个 @ManyToMany 注解来描述。
  3. getAuthorities 方法返回用户的角色信息,我们在这个方法中把自己的 Role 稍微转化一下即可。

数据模型准备好之后,我们再来定义一个 UserDao:

public interface UserDao extends JpaRepository<User,Long> {
    User findUserByUsername(String username);
}

接下来定义 UserService ,如下:

@Service
public class UserService implements UserDetailsService {
    @Autowired
    UserDao userDao;
  
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userDao.findUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return user;
    }
}

我们自己定义的 UserService 需要实现 UserDetailsService 接口,实现该接口,就要实现接口中的方法,也就是 loadUserByUsername ,这个方法的参数就是用户在登录的时候传入的用户名,根据用户名去查询用户信息(查出来之后,系统会自动进行密码比对)。

在 SecurityConfig 中,我们通过如下方式来配置用户:

@Autowired
UserService userService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService);
}

最后,我们再在 application.properties 中配置一下数据库和 JPA 的基本信息,如下:

spring.datasource.username=root
spring.datasource.password=123
spring.datasource.url=jdbc:mysql:///withjpa?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

spring.jpa.database=mysql
spring.jpa.database-platform=mysql
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect

11.自动登录

在 Spring Security 的配置中,添加如下代码即可:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .rememberMe()
            .and()
            .csrf().disable();
}

由于我们自己没有设置 key,key 默认值是一个 UUID 字符串,这样会带来一个问题,就是如果服务端重启,这个 key 会变,这样就导致之前派发出去的所有 remember-me 自动登录令牌失效,所以,我们可以指定这个 key。指定方式如下:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .rememberMe()
            .key("wly")
            .and()
            .csrf().disable();
}

最后的 key 是一个散列盐值,可以用来防治令牌被修改。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值