第五节 集成Spring Security

学习源码:https://github.com/xbhou-cn/spring-security-for-learn

目录

Spring Security简介

Spring Boot 集成 Spring Security

Spring security基本原理

UserDetailsService接口讲解

PasswordEncoder接口

用户认证

自定义登陆画面和设置不需要认证的访问

基于角色或权限进行访问控制

hasAuthority方法

hasAnyAuthority方法

hasRole方法

hasAnyRole方法

自定义没有访问权限画面

注解的使用

@Secured

@PreAuthorize

@PostAuthorize

@PreFilter和@PostFilter

退出操作

基于数据库实现自动登陆


Spring Security简介

Spring Security致力于为Java应用提供认证和授权管理。它是一个强大的,高度自定义的认证和访问控制框架。

具体介绍参见https://docs.spring.io/spring-security/site/docs/5.0.5.RELEASE/reference/htmlsingle/

这句话包括两个关键词:Authentication(认证)和 Authorization(授权,也叫访问控制)

认证:系统认为用户是否能登陆

授权:系统判定用户是否有权限做某事

Spring Security是重量级框架,各方面功能比较全面,还有一种常用的轻量级安全框架shiro,常用的安全管理技术栈的组合是这样的:

  • SSM+Shiro
  • Spring Boot/Spring Cloud +Spring Security

核心模块:

Core - spring-security-core.jar

Remoting - spring-security-remoting.jar

Web - spring-security-web.jar

Config - spring-security-config.jar

LDAP - spring-security-ldap.jar

OAuth 2.0 Core - spring-security-oauth2-core.jar

OAuth 2.0 Client - spring-security-oauth2-client.jar

OAuth 2.0 JOSE - spring-security-oauth2-jose.jar

ACL - spring-security-acl.jar

CAS - spring-security-cas.jar

OpenID - spring-security-openid.jar

Test - spring-security-test.jar

Spring Boot 集成 Spring Security

第一步:引入Spring Security所需依赖

<!--Spring boot 安全框架-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

第二步:编写测试类

package xb.hou.modules.security.rest;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @title: TestController
 * @Author xbhou
 * @Date: 2021-04-22 22:43
 * @Version 1.0
 */
@RestController
@RequestMapping("/test")
public class TestController {
    @GetMapping("/hello")
    public String hello() {
        return "Hello World";
    }
}

第三步:进行测试,访问http://localhost:8000/test/hello,发现画面跳转到登陆画面,说明集成成功,需要认证之后才能访问,默认用户名是user,密码在项目启动时在控制台打印出了,认证后跳转到访问的接口

Spring security基本原理

Spring security本质上就是过滤器链

UserDetailsService接口讲解

自定义逻辑控制认证功能,继承该接口,重写查询用户的逻辑进行认证

过程:

  1. 继承UsernamePasswordAuthenticationFilter,重写attemptAuthentication(用户验证),successfulAuthentication(认证成功),unsuccessfulAuthentication(认证失败方法)
  2. 创建类实现UserDetailsService接口,编写查询数据过程,返回User对象(或者创建自定义对象继承User)

PasswordEncoder接口

对密码进行加密

@Bean
public PasswordEncoder passwordEncoder() {
	// 密码加密方式
	return new BCryptPasswordEncoder();
}

用户认证

  • 通过配置文件
spring:
  security:
    user:
      name: admin
      password: admin
  • 通过配置类
package xb.hou.modules.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @title: SecurityConfig
 * @Author xbhou
 * @Date: 2021-04-25 22:14
 * @Version 1.0
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private PasswordEncoder encoder;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 重写该方法进行用户认证配置
        auth.inMemoryAuthentication().withUser("admin").password(encoder.encode("admin")).roles("admin");
    }

    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
  • 自定义编写实现实现UserDetailsService
    • 第一步 创建配置类,设置使用哪一个UserDetailsService实现类
package xb.hou.modules.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;

/**
 * @title: SecurityConfig
 * @Author xbhou
 * @Date: 2021-04-25 22:14
 * @Version 1.0
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userService;
    @Autowired
    private PasswordEncoder passwordEncoder;

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

    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
  • 第二步 编写实现类, 返回User对象,User对象用用户名,密码和操作权限
package xb.hou.modules.security.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * @title: UserDetailsServiceImpl
 * @Author xbhou
 * @Date: 2021-04-25 22:28
 * @Version 1.0
 */
@Service("userService")
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(() -> "admin");
        // 抛出UsernameNotFoundException则表示用户不存在,认证不通过
        return new User("admin", passwordEncoder.encode("admin"), authorities);
    }
}
  • 查询数据库的方式下认证
package xb.hou.modules.security.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import xb.hou.modules.security.domain.SysUser;
import xb.hou.modules.security.repository.UserRepository;

import java.util.ArrayList;
import java.util.List;

/**
 * @title: UserDetailsServiceImpl
 * @Author xbhou
 * @Date: 2021-04-25 22:28
 * @Version 1.0
 */
@Service("userService")
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        // 通过用户名查询用户
        SysUser user = userRepository.findByUserName(userName);
        if (user == null) {
            // 用户不存在抛出异常
            throw new UsernameNotFoundException("用户不存在!");
        }
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(() -> "admin");
        // 返回数据库查询到的数据
        return new User(user.getUserName(), passwordEncoder.encode(user.getPassword()), authorities);
    }
}

自定义登陆画面和设置不需要认证的访问

  • 在配置类实现相关配置
package xb.hou.modules.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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;

/**
 * @title: SecurityConfig
 * @Author xbhou
 * @Date: 2021-04-25 22:14
 * @Version 1.0
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userService;
    @Autowired
    private PasswordEncoder passwordEncoder;

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() //自定义自己编写的登陆画面
                .loginPage("/login.html") //登陆画面
                .loginProcessingUrl("/user/login") //登陆访问路径,不用我们自己实现
                .defaultSuccessUrl("/test/index").permitAll() //登陆成功 跳转路径
                .and().authorizeRequests().antMatchers("/", "/test/hello", "/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
                .anyRequest().authenticated()
                .and().csrf().disable(); //关闭csrf防护
    }

    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
  • 创建登陆画面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陆</title>
</head>
<body>
<!--请求方式必须是post,请求地址要和配置的地址相同-->
<form action="/user/login" method="post">
    <!--用户名和密码输入框的name必须是username和password-->
    用户名:<input type="text" name="username"/>
    <br/>
    密码:<input type="password" name="password"/>
    <br/>
    <input type="submit" value="login"/>
</form>
</body>
</html>

基于角色或权限进行访问控制

hasAuthority方法

如果当前的主体具有指定的权限,则返回true,否则返回false,一般只针对一个权限操作

  • 在配置类设置当前访问地址有哪些权限

package xb.hou.modules.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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;

/**
 * @title: SecurityConfig
 * @Author xbhou
 * @Date: 2021-04-25 22:14
 * @Version 1.0
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userService;
    @Autowired
    private PasswordEncoder passwordEncoder;

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() //自定义自己编写的登陆画面
                .loginPage("/login.html") //登陆画面
                .loginProcessingUrl("/user/login") //登陆访问路径,不用我们自己实现
                .defaultSuccessUrl("/test/index").permitAll() //登陆成功 跳转路径
                .and().authorizeRequests().antMatchers("/", "/test/hello", "/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
                // 只有用户具有admin权限才能访问该路径
                .antMatchers("/test/index").hasAuthority("admin")
                .anyRequest().authenticated()
                .and().csrf().disable(); //关闭csrf防护
    }

    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
  • 在UserDetailsService,把返回的User对象设置权限

package xb.hou.modules.security.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import xb.hou.modules.security.domain.SysUser;
import xb.hou.modules.security.repository.UserRepository;

import java.util.ArrayList;
import java.util.List;

/**
 * @title: UserDetailsServiceImpl
 * @Author xbhou
 * @Date: 2021-04-25 22:28
 * @Version 1.0
 */
@Service("userService")
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        // 通过用户名查询用户
        SysUser user = userRepository.findByUserName(userName);
        if (user == null) {
            // 用户不存在抛出异常
            throw new UsernameNotFoundException("用户不存在!");
        }
        List<GrantedAuthority> authorities = new ArrayList<>();
        // 给用户添加admin权限
        authorities.add(() -> "admin");
        // 返回数据库查询到的数据
        return new User(user.getUserName(), passwordEncoder.encode(user.getPassword()), authorities);
    }
}

hasAnyAuthority方法

如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回true

  • 在配置类设置当前访问地址有哪些权限
package xb.hou.modules.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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;

/**
 * @title: SecurityConfig
 * @Author xbhou
 * @Date: 2021-04-25 22:14
 * @Version 1.0
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userService;
    @Autowired
    private PasswordEncoder passwordEncoder;

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() //自定义自己编写的登陆画面
                .loginPage("/login.html") //登陆画面
                .loginProcessingUrl("/user/login") //登陆访问路径,不用我们自己实现
                .defaultSuccessUrl("/test/index").permitAll() //登陆成功 跳转路径
                .and().authorizeRequests().antMatchers("/", "/test/hello", "/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
                // 用户具有admin或者test权限都能访问该路径
                .antMatchers("/test/index").hasAnyAuthority("admin,test")
                .anyRequest().authenticated()
                .and().csrf().disable(); //关闭csrf防护
    }

    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
  • 在UserDetailsService,把返回的User对象设置权限,同上

hasRole方法

  • 在配置类设置当前访问地址有哪些权限
// 用户具有sale角色权限才能访问,会自动添加ROLE_
.antMatchers("/test/index").hasRole("sale")
  • 在UserDetailsService,把返回的User对象设置权限,同上
// 添加sale角色
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale");

hasAnyRole方法

满足其中一个配置条件,配置和实现方式同上

自定义没有访问权限画面

配置没有访问权限需要跳转的画面

@Override
protected void configure(HttpSecurity http) throws Exception {
	//没有访问权限跳转的画面
	http.exceptionHandling().accessDeniedPage("/unauth.html");
}

注解的使用

方法授权类型

声明方式

JSR标准

允许SpEL表达式

@PreAuthorize

@PostAuthorize

注解

@EnableGlobalMethodSecurity(prePostEnabled = true)

No

Yes

@RolesAllowed

@PermitAll

@DenyAll

注解

@EnableGlobalMethodSecurity(jsr250Enabled = true)

Yes

NO

@Secure

注解

@EnableGlobalMethodSecurity(securedEnabled = true)

No

No

@Secured

判断是否具有角色,另外注意的是这里匹配的字符串需要添加前缀"ROLE_"。

@Secured({"ROLE_ADMIN"})

使用注解前先要开启注解功能

@EnableGlobalMethodSecurity(securedEnabled = true)
package xb.hou;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

/**
 * @title: AppRun
 * @Author xbhou
 * @Date: 2021-04-16 17:20
 * @Version 1.0
 */
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class AppRun {
    public static void main(String[] args) {
        SpringApplication.run(AppRun.class, args);
    }
}

@PreAuthorize

注解适合进入方法前的校验,@PreAuthorize可以将登陆用户的roles/permissions参数传到方法中

@GetMapping("/hello")
@PreAuthorize("hasAnyAuthority('menu:system')")
public String hello() {
	return "Hello World";
}

使用注解前先要开启注解功能

@EnableGlobalMethodSecurity(prePostEnabled = true)

@PostAuthorize

注解使用并不多,在方法执行后再进行权限认证,适合验证带有返回值的权限

@GetMapping("/hello")
@PostAuthorize("hasAnyAuthority('menu:system')")
public String hello() {
	return "Hello World";
}

开启方式同上

@PreFilter和@PostFilter

进入控制器前和权限验证之后对数据进行过滤,一般用到的不多

@PreFilter在执行方法之前过滤集合或数组.

@PostMapping("/hello")
@PreFilter(value = "filterObject%2==0")
public String hello(@RequestBody List<Integer> num) {
	System.out.println(num);
	return "Hello";
}


// 参数是 [1,2,3,4,5]
// 运行打印的结果是[2, 4]

@PostFilter执行该方法后,过滤返回的集合或数组.

@GetMapping("/hello")
@PostFilter(value = "filterObject.username=='admin'")
public List<User> hello() {
	List<User> u = new ArrayList<>();
	u.add(new User("admin", "password", AuthorityUtils.commaSeparatedStringToAuthorityList("")));
	u.add(new User("admin1", "password1", AuthorityUtils.commaSeparatedStringToAuthorityList("")));
	System.out.println(u);
	return u;
}

退出操作

  • 在配置类中进行配置
@Override
protected void configure(HttpSecurity http) throws Exception {
	//配置退出
	http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll();
	// 配置没有权限跳转的画面
	http.exceptionHandling().accessDeniedPage("/unauth.html");
	http.formLogin() //自定义自己编写的登陆画面
			.loginPage("/login.html") //登陆画面
			.loginProcessingUrl("/user/login") //登陆访问路径,不用我们自己实现
			.defaultSuccessUrl("/success.html").permitAll() //登陆成功 跳转路径
			.and().authorizeRequests().antMatchers("/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
			.anyRequest().authenticated()
			.and().csrf().disable(); //关闭csrf防护
}
  • 登陆成功后跳转到success.html,退出直接访问/logout
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陆成功</title>
</head>
<body>
<a href="/logout">退出</a>
</body>
</html>

测试是访问login.html,访问/test/index的话,登陆成功后会继续访问该url,会覆盖配置的登陆成功需要访问的路径

@GetMapping("/index")
@PreAuthorize("hasAnyAuthority('menu:list')")
public String index() {
	System.out.println("nihao");
	return "Hello index";
}

基于数据库实现自动登陆

自动登陆的实现方式:

  • cokie技术
  • 基于安全框架的自动登陆

原理:

  1. 认证成功后,后台返回前端一段加密串,用cookie保存,后台把加密串和用户信息保存在数据库
  2. cookie可以设置有效时长,再次访问时,拿加密串到后台获取用户信息

流程 

  1. 首次认证的源码解析
package org.springframework.security.web.authentication;

...

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
    protected ApplicationEventPublisher eventPublisher;
    protected AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
    private AuthenticationManager authenticationManager;
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private RememberMeServices rememberMeServices = new NullRememberMeServices();
    private RequestMatcher requiresAuthenticationRequestMatcher;
    private boolean continueChainBeforeSuccessfulAuthentication = false;
    private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
    private boolean allowSessionCreation = true;
    private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
    private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();

    ...

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        if (!this.requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
        } else {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Request is to process authentication");
            }
            // 认证方法
            ...
            // 认证通过后调用该方法
            this.successfulAuthentication(request, response, chain, authResult);
        }
    }
    ...

    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
        }

        SecurityContextHolder.getContext().setAuthentication(authResult);
        // 认证成功,将token写入数据库
        this.rememberMeServices.loginSuccess(request, response, authResult);
        if (this.eventPublisher != null) {
            this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }

        this.successHandler.onAuthenticationSuccess(request, response, authResult);
    }
    ...
}
...

public abstract class AbstractRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
    public static final String SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY = "remember-me";
    public static final String DEFAULT_PARAMETER = "remember-me";
    public static final int TWO_WEEKS_S = 1209600;
    private static final String DELIMITER = ":";
    protected final Log logger = LogFactory.getLog(this.getClass());
    protected final MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private UserDetailsService userDetailsService;
    private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
    private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
    private String cookieName = "remember-me";
    private String cookieDomain;
    private String parameter = "remember-me";
    private boolean alwaysRemember;
    private String key;
    private int tokenValiditySeconds = 1209600;
    private Boolean useSecureCookie = null;
    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    protected AbstractRememberMeServices(String key, UserDetailsService userDetailsService) {
        Assert.hasLength(key, "key cannot be empty or null");
        Assert.notNull(userDetailsService, "UserDetailsService cannot be null");
        this.key = key;
        this.userDetailsService = userDetailsService;
    }

    public final void loginFail(HttpServletRequest request, HttpServletResponse response) {
        this.logger.debug("Interactive login attempt was unsuccessful.");
        this.cancelCookie(request, response);
        this.onLoginFail(request, response);
    }

    protected void onLoginFail(HttpServletRequest request, HttpServletResponse response) {
    }

    public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        if (!this.rememberMeRequested(request, this.parameter)) {
            this.logger.debug("Remember-me login not requested.");
        } else {
            // 到达这一步,继续向下执行,到PersistentTokenBasedRememberMeServices
            this.onLoginSuccess(request, response, successfulAuthentication);
        }
    }

    protected abstract void onLoginSuccess(HttpServletRequest var1, HttpServletResponse var2, Authentication var3);
}
...

public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {
    private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();
    private SecureRandom random = new SecureRandom();
    public static final int DEFAULT_SERIES_LENGTH = 16;
    public static final int DEFAULT_TOKEN_LENGTH = 16;
    private int seriesLength = 16;
    private int tokenLength = 16;

    public PersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
        super(key, userDetailsService);
        this.tokenRepository = tokenRepository;
    }

    protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
        if (cookieTokens.length != 2) {
            throw new InvalidCookieException("Cookie token did not contain 2 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
        } else {
            String presentedSeries = cookieTokens[0];
            String presentedToken = cookieTokens[1];
            PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
            if (token == null) {
                throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
            } else if (!presentedToken.equals(token.getTokenValue())) {
                this.tokenRepository.removeUserTokens(token.getUsername());
                throw new CookieTheftException(this.messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen", "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
            } else if (token.getDate().getTime() + (long) this.getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
                throw new RememberMeAuthenticationException("Remember-me login has expired");
            } else {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Refreshing persistent login token for user '" + token.getUsername() + "', series '" + token.getSeries() + "'");
                }

                PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date());

                try {
                    this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
                    this.addCookie(newToken, request, response);
                } catch (Exception var9) {
                    this.logger.error("Failed to update token: ", var9);
                    throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
                }

                return this.getUserDetailsService().loadUserByUsername(token.getUsername());
            }
        }
    }

    protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        String username = successfulAuthentication.getName();
        this.logger.debug("Creating new persistent login for user " + username);
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());

        try {
            // 通过PersistentTokenRepository创建token,存入数据库
            // 因此,想要把token存入数据库,则需要实例化JdbcTokenRepositoryImpl,设置DataSource
            this.tokenRepository.createNewToken(persistentToken);
            this.addCookie(persistentToken, request, response);
        } catch (Exception var7) {
            this.logger.error("Failed to save persistent token ", var7);
        }
    }
    ...
}

2. 实现UserDetailsService,并配置用户登陆查询方式

  • 实现UserDetailsService
package xb.hou.service;

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
    final PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        return new User("admin", passwordEncoder.encode("admin"), AuthorityUtils.commaSeparatedStringToAuthorityList("user:list"));
    }
}

  • 指定用户登陆的查询方式
package xb.hou.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

/**
 * @title: SecurityConfig
 * @Author xbhou
 * @Date: 2021-06-17 13:23
 * @Version 1.0
 */
@Configuration
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    final private PasswordEncoder passwordEncoder;
    final private UserDetailsService userDetailsService;
    final private PersistentTokenRepository repository;

    /**
     * 重写该方法,并指定用户查询实现方式
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //配置退出
        http.logout().logoutUrl("/logout").logoutSuccessUrl("/user/logout").permitAll();
        // 没有权限跳转到unauth.html
        http.exceptionHandling().accessDeniedPage("/unauth.html");
        // 登陆方式
        http.formLogin().loginPage("/login.html") //登陆画面
                .loginProcessingUrl("/user/login") //登陆的form的action
                .defaultSuccessUrl("/success.html").permitAll() // 登陆成功后跳转的画面
                .and().authorizeRequests().anyRequest().authenticated() // 其他访问需要认证,以及授权
                .and().rememberMe().tokenRepository(repository) // token记录方式
                .tokenValiditySeconds(3600) // 过期时间,单位为秒
                .and().csrf().disable();
    }
}

  • login.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陆</title>
</head>
<body>
<!--请求方式必须是post,请求地址要和配置的地址相同-->
<form action="/user/login" method="post">
    <!--用户名和密码输入框的name必须是username和password-->
    用户名:<input type="text" name="username"/>
    <br/>
    密码:<input type="password" name="password"/>
    <br/>
    <div>
        <!--自动登陆的name必须是remember-me-->
        <label><input type="checkbox" name="remember-me"/>自动登录</label>
        <button type="submit">立即登陆</button>
    </div>
</form>
</body>
</html>
  • success.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陆成功</title>
</head>
<body>
<a href="/logout">退出</a>
</body>
</html>
  • unauth.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>错误</title>
</head>
<body>
<h1>没有访问权限!</h1>
</body>
</html>

3. 配置数据库连接,实例化JdbcTokenRepositoryImpl,设置DataSource

  • 数据库配置信息
server:
  port: 8000

#配置数据源
spring:
  main:
    allow-bean-definition-overriding: true
  #配置 Jpa
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      dialect: org.hibernate.dialect.MySQL5Dialect
    open-in-view: true
  datasource:
    druid:
      db-type: com.alibaba.druid.pool.DruidDataSource
      driverClassName: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost/demo?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: root
      # 初始连接数
      initial-size: 5
      # 最小连接数
      min-idle: 15
      # 最大连接数
      max-active: 30
      # 超时时间(以秒数为单位)
      remove-abandoned-timeout: 180
      # 获取连接超时时间
      max-wait: 3000
      # 连接有效性检测时间
      time-between-eviction-runs-millis: 60000
      # 连接在池中最小生存的时间
      min-evictable-idle-time-millis: 300000
      # 连接在池中最大生存的时间
      max-evictable-idle-time-millis: 900000
      # 指明连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除
      test-while-idle: true
      # 指明是否在从池中取出连接前进行检验,如果检验失败, 则从池中去除连接并尝试取出另一个
      test-on-borrow: true
      # 是否在归还到池中前进行检验
      test-on-return: false
      # 检测连接是否有效
      validation-query: select 1
  • 实例化JdbcTokenRepositoryImpl,设置DataSource
    • 使用JdbcTokenRepositoryImpl需要先建表,或者setCreateTableOnStartup为true
        CREATE TABLE persistent_logins (
        username VARCHAR ( 64 ) NOT NULL,
        series VARCHAR ( 64 ) PRIMARY KEY,
        token VARCHAR ( 64 ) NOT NULL,
        last_used TIMESTAMP NOT NULL)
    
package xb.hou.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;

/**
 * @title: BeanConfig
 * @Author xbhou
 * @Date: 2021-06-24 16:40
 * @Version 1.0
 */
@Configuration
@RequiredArgsConstructor
public class BeanConfig {
    final private DataSource dataSource;

    /**
     * 实例化JdbcTokenRepositoryImpl,设置DataSource
     * @return
     */
    @Bean
    public PersistentTokenRepository getRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        return jdbcTokenRepository;
    }

    /**
     * 加密方式
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

4. 记住密码再次登陆时,源码解析

  • RememberMeAuthenticationFilter
package org.springframework.security.web.authentication.rememberme;

...
public class RememberMeAuthenticationFilter extends GenericFilterBean implements ApplicationEventPublisherAware {
    private ApplicationEventPublisher eventPublisher;
    private AuthenticationSuccessHandler successHandler;
    private AuthenticationManager authenticationManager;
    private RememberMeServices rememberMeServices;

    public RememberMeAuthenticationFilter(AuthenticationManager authenticationManager, RememberMeServices rememberMeServices) {
        Assert.notNull(authenticationManager, "authenticationManager cannot be null");
        Assert.notNull(rememberMeServices, "rememberMeServices cannot be null");
        this.authenticationManager = authenticationManager;
        this.rememberMeServices = rememberMeServices;
    }

    public void afterPropertiesSet() {
        Assert.notNull(this.authenticationManager, "authenticationManager must be specified");
        Assert.notNull(this.rememberMeServices, "rememberMeServices must be specified");
    }

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            // 调用rememberMeServices的autoLogin方法进行登陆,进入autoLogin方法
            Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
            if (rememberMeAuth != null) {
                try {
                    rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
                    SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
                    this.onSuccessfulAuthentication(request, response, rememberMeAuth);
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug("SecurityContextHolder populated with remember-me token: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
                    }

                    if (this.eventPublisher != null) {
                        this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(SecurityContextHolder.getContext().getAuthentication(), this.getClass()));
                    }

                    if (this.successHandler != null) {
                        this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
                        return;
                    }
                } catch (AuthenticationException var8) {
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug("SecurityContextHolder not populated with remember-me token, as AuthenticationManager rejected Authentication returned by RememberMeServices: '" + rememberMeAuth + "'; invalidating remember-me token", var8);
                    }

                    this.rememberMeServices.loginFail(request, response);
                    this.onUnsuccessfulAuthentication(request, response, var8);
                }
            }

            chain.doFilter(request, response);
        } else {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
            }

            chain.doFilter(request, response);
        }

    }

    protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) {
    }

    protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
    }

    public RememberMeServices getRememberMeServices() {
        return this.rememberMeServices;
    }
}
  • AbstractRememberMeServices
package org.springframework.security.web.authentication.rememberme;

public abstract class AbstractRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
    public static final String SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY = "remember-me";
    public static final String DEFAULT_PARAMETER = "remember-me";
    public static final int TWO_WEEKS_S = 1209600;
    private static final String DELIMITER = ":";
    protected final Log logger = LogFactory.getLog(this.getClass());
    protected final MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private UserDetailsService userDetailsService;
    private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
    private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
    private String cookieName = "remember-me";
    private String cookieDomain;
    private String parameter = "remember-me";
    private boolean alwaysRemember;
    private String key;
    private int tokenValiditySeconds = 1209600;
    private Boolean useSecureCookie = null;
    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    protected AbstractRememberMeServices(String key, UserDetailsService userDetailsService) {
        Assert.hasLength(key, "key cannot be empty or null");
        Assert.notNull(userDetailsService, "UserDetailsService cannot be null");
        this.key = key;
        this.userDetailsService = userDetailsService;
    }

    public void afterPropertiesSet() {
        Assert.hasLength(this.key, "key cannot be empty or null");
        Assert.notNull(this.userDetailsService, "A UserDetailsService is required");
    }

    /**
     * 自动登陆方法
     * @param request
     * @param response
     * @return
     */
    public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
        // 获得cookie
        String rememberMeCookie = this.extractRememberMeCookie(request);
        if (rememberMeCookie == null) {
            return null;
        } else {
            this.logger.debug("Remember-me cookie detected");
            if (rememberMeCookie.length() == 0) {
                this.logger.debug("Cookie was empty");
                this.cancelCookie(request, response);
                return null;
            } else {
                UserDetails user = null;

                try {
                    // 对cookie解密
                    String[] cookieTokens = this.decodeCookie(rememberMeCookie);
                    // 通过cookie获取用户
                    user = this.processAutoLoginCookie(cookieTokens, request, response);
                    // 验证用户
                    this.userDetailsChecker.check(user);
                    this.logger.debug("Remember-me cookie accepted");
                    // 
                    return this.createSuccessfulAuthentication(request, user);
                } catch (CookieTheftException var6) {
                    this.cancelCookie(request, response);
                    throw var6;
                } catch (UsernameNotFoundException var7) {
                    this.logger.debug("Remember-me login was valid but corresponding user not found.", var7);
                } catch (InvalidCookieException var8) {
                    this.logger.debug("Invalid remember-me cookie: " + var8.getMessage());
                } catch (AccountStatusException var9) {
                    this.logger.debug("Invalid UserDetails: " + var9.getMessage());
                } catch (RememberMeAuthenticationException var10) {
                    this.logger.debug(var10.getMessage());
                }

                this.cancelCookie(request, response);
                return null;
            }
        }
    }

    ...

    protected abstract UserDetails processAutoLoginCookie(String[] var1, HttpServletRequest var2, HttpServletResponse var3) throws RememberMeAuthenticationException, UsernameNotFoundException;

    ...
}
  • PersistentTokenBasedRememberMeServices
package org.springframework.security.web.authentication.rememberme;

import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.util.Assert;

public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {
    private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();
    private SecureRandom random = new SecureRandom();
    public static final int DEFAULT_SERIES_LENGTH = 16;
    public static final int DEFAULT_TOKEN_LENGTH = 16;
    private int seriesLength = 16;
    private int tokenLength = 16;

    public PersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
        super(key, userDetailsService);
        this.tokenRepository = tokenRepository;
    }

    protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
        if (cookieTokens.length != 2) {
            throw new InvalidCookieException("Cookie token did not contain 2 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
        } else {
            String presentedSeries = cookieTokens[0];
            String presentedToken = cookieTokens[1];
            // 从数据库查询token信息
            PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
            if (token == null) {
                throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
            } else if (!presentedToken.equals(token.getTokenValue())) {
                this.tokenRepository.removeUserTokens(token.getUsername());
                throw new CookieTheftException(this.messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen", "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
            } else if (token.getDate().getTime() + (long)this.getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
                throw new RememberMeAuthenticationException("Remember-me login has expired");
            } else {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Refreshing persistent login token for user '" + token.getUsername() + "', series '" + token.getSeries() + "'");
                }

                // 每次登陆更新数据库
                PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date());

                try {
                    this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
                    this.addCookie(newToken, request, response);
                } catch (Exception var9) {
                    this.logger.error("Failed to update token: ", var9);
                    throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
                }

                // 通过UserDetailsService获取用户
                return this.getUserDetailsService().loadUserByUsername(token.getUsername());
            }
        }
    }
    ...
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值