登陆的时候,请求发送给过滤器UsernamePasswordAuthenticationFilter,当该过滤器认证成功后,会调用RememberMeService,会生成一个token,将token写入到浏览器cookie,同时RememberMeService里边还有个TokenRepository,将token和用户信息写入到数据库中。这样当用户再次访问系统,访问某一个接口时,会通过一个RememberMeAuthenticationFilter的过滤器,他会读取cookie中的token,交给RememberService,RememberService会用TokenRepository根据token从数据库中查是否有记录,若是有记录会把用户名取出来,再调用UserDetailService根据用户名获取用户信息,而后放在SecurityContext里。php
RememberMeAuthenticationFilter在Spring Security中认证过滤器链的倒数第二个过滤器位置,当其余认证过滤器都无法认证成功的时候,就会调用RememberMeAuthenticationFilter尝试认证。html
实现:java
1,登陆表单加上,SpringSecurity在SpringSessionRememberMeServices类里定义了一个常量,默认值就是remember-memysql
2,根据上边的原理图可知,要配置TokenRepository,把生成的token存进数据库,这是一个配置bean的配置,放在了BrowserSecurityConfig里web
3,在configure里配置spring
4,在BrowserProperties里加上自动登陆时间,把记住我时间作成可配置的sql
//记住我秒数配置 private int rememberMeSeconds = 10;数据库
pom.xml:apache
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>urity</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-jasper -->
<!--配置支持jsp-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>8.5.12</version>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.servlet/jstl -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!--添加static和templates的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<!-- 因为我使用的spring boot因此我是引入spring-boot-starter-security并且我使用了spring io因此不须要填写依赖的版本号 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.22</version>
</dependency>
<!--mybatis与mysql-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--druid依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.25</version>
</dependency>
<!-- spring social相关 -->
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-config -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
复制代码
SecurityConfiguration:api
package urity.demo.config;
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.builders.WebSecurity;
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;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.annotation.Resource;
import javax.sql.DataSource;
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Resource
private DataSource dataSource;
@Resource
private UserDetailsService myUserDetailsService;
/** * 配置TokenRepository * * @return */
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
// 配置数据源
jdbcTokenRepository.setDataSource(dataSource);
// 第一次启动的时候自动建表(能够不用这句话,本身手动建表,源码中有语句的)
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
// 处理密码加密解密逻辑
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//验证相关
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
//浏览器相关
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/hello", "/login.html").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
//指定登陆页的路径
.loginPage("/hello")
//指定自定义form表单请求的路径
.loginProcessingUrl("/authentication/form")
.failureUrl("/login?error")
.defaultSuccessUrl("/success")
//必须容许全部用户访问咱们的登陆页(例如未验证的用户,不然验证流程就会进入死循环)
//这个formLogin().permitAll()方法容许全部用户基于表单登陆访问/login这个page。
.permitAll()
.and()
.rememberMe() // 记住我相关配置
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(1209600)
;
//默认都会产生一个hiden标签 里面有安全相关的验证 这边咱们不须要 可禁用掉
http.csrf().disable();
}
//web安全相关
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
}
复制代码
MyUserDetailService:
package urity.demo.support;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.Component;
import urity.demo.entity.User;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
//自定义用户处理的逻辑
//用户的信息的service
@Component
public class MyUserDetailService implements UserDetailsService {
/** * 日志处理类 */
private org.slf4j.Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private PasswordEncoder passwordEncoder;
/** * 根据用户名加载用户信息 * * @param username 用户名 * @return UserDetails * @throws UsernameNotFoundException */
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("表单登陆用户名:" + username);
System.out.println("表单登陆用户名:" + username);
List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
grantedAuthorityList.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return "admin";
}
});
User user = new User();
user.setUsername("test");
user.setPassword("123");
String pWord =passwordEncoder.encode(user.getPassword());
System.out.println("表单登陆加密后密码:" + pWord);
System.out.println("库中的username:"+user.getUsername());
if(username.equals(user.getUsername())) {
MyUser myUser = new MyUser(user.getUsername(), pWord, true, true, true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_USER"));
return myUser;
}else {
throw new UsernameNotFoundException("用户["+username+"]不存在");
}
}
}
复制代码
MyUser:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.Assert;
import java.io.Serializable;
import java.util.*;
import java.util.function.Function;
public class MyUser implements UserDetails, CredentialsContainer {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// ~ Instance fields
// ================================================================================================
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
// ~ Constructors
// ===================================================================================================
/** * Calls the more complex constructor with all boolean arguments set to {@code true}. */
public MyUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
/** * Construct the <code>User</code> with the details required by * {@link org.springframework.security.authentication.dao.DaoAuthenticationProvider}. * * @param username the username presented to the * <code>DaoAuthenticationProvider</code> * @param password the password that should be presented to the * <code>DaoAuthenticationProvider</code> * @param enabled set to <code>true</code> if the user is enabled * @param accountNonExpired set to <code>true</code> if the account has not expired * @param credentialsNonExpired set to <code>true</code> if the credentials have not * expired * @param accountNonLocked set to <code>true</code> if the account is not locked * @param authorities the authorities that should be granted to the caller if they * presented the correct username and password and the user is enabled. Not null. * * @throws IllegalArgumentException if a <code>null</code> value was passed either as * a parameter or as an element in the <code>GrantedAuthority</code> collection */
public MyUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
if (((username == null) || "".equals(username)) || (password == null)) {
throw new IllegalArgumentException(
"Cannot pass null or empty values to constructor");
}
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}
// ~ Methods
// ========================================================================================================
public Collection<GrantedAuthority> getAuthorities() {
return authorities;
}
public String getPassword() {
return password;
}
public String getUsername() {
return username;
}
public boolean isEnabled() {
return enabled;
}
public boolean isAccountNonExpired() {
return accountNonExpired;
}
public boolean isAccountNonLocked() {
return accountNonLocked;
}
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
public void eraseCredentials() {
password = null;
}
private static SortedSet<GrantedAuthority> sortAuthorities( Collection<? extends GrantedAuthority> authorities) {
Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");
// Ensure array iteration order is predictable (as per
// UserDetails.getAuthorities() contract and SEC-717)
SortedSet<GrantedAuthority> sortedAuthorities = new TreeSet<GrantedAuthority>(
new MyUser.AuthorityComparator());
for (GrantedAuthority grantedAuthority : authorities) {
Assert.notNull(grantedAuthority,
"GrantedAuthority list cannot contain any null elements");
sortedAuthorities.add(grantedAuthority);
}
return sortedAuthorities;
}
private static class AuthorityComparator implements Comparator<GrantedAuthority>, Serializable {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
public int compare(GrantedAuthority g1, GrantedAuthority g2) {
// Neither should ever be null as each entry is checked before adding it to
// the set.
// If the authority is null, it is a custom authority and should precede
// others.
if (g2.getAuthority() == null) {
return -1;
}
if (g1.getAuthority() == null) {
return 1;
}
return g1.getAuthority().compareTo(g2.getAuthority());
}
}
/** * Returns {@code true} if the supplied object is a {@code User} instance with the * same {@code username} value. * <p> * In other words, the objects are equal if they have the same username, representing * the same principal. */
@Override
public boolean equals(Object rhs) {
if (rhs instanceof MyUser) {
return username.equals(((MyUser) rhs).username);
}
return false;
}
/** * Returns the hashcode of the {@code username}. */
@Override
public int hashCode() {
return username.hashCode();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(super.toString()).append(": ");
sb.append("Username: ").append(this.username).append("; ");
sb.append("Password: [PROTECTED]; ");
sb.append("Enabled: ").append(this.enabled).append("; ");
sb.append("AccountNonExpired: ").append(this.accountNonExpired).append("; ");
sb.append("credentialsNonExpired: ").append(this.credentialsNonExpired)
.append("; ");
sb.append("AccountNonLocked: ").append(this.accountNonLocked).append("; ");
if (!authorities.isEmpty()) {
sb.append("Granted Authorities: ");
boolean first = true;
for (GrantedAuthority auth : authorities) {
if (!first) {
sb.append(",");
}
first = false;
sb.append(auth);
}
}
else {
sb.append("Not granted any authorities");
}
return sb.toString();
}
public static MyUser.UserBuilder withUsername(String username) {
return new MyUser.UserBuilder().username(username);
}
/** * Builds the user to be added. At minimum the username, password, and authorities * should provided. The remaining attributes have reasonable defaults. */
public static class UserBuilder {
private String username;
private String password;
private List<GrantedAuthority> authorities;
private boolean accountExpired;
private boolean accountLocked;
private boolean credentialsExpired;
private boolean disabled;
/** * Creates a new instance */
private UserBuilder() {
}
/** * Populates the username. This attribute is required. * * @param username the username. Cannot be null. * @return the {@link User.UserBuilder} for method chaining (i.e. to populate * additional attributes for this user) */
private MyUser.UserBuilder username(String username) {
Assert.notNull(username, "username cannot be null");
this.username = username;
return this;
}
/** * Populates the password. This attribute is required. * * @param password the password. Cannot be null. * @return the {@link User.UserBuilder} for method chaining (i.e. to populate * additional attributes for this user) */
public MyUser.UserBuilder password(String password) {
Assert.notNull(password, "password cannot be null");
this.password = password;
return this;
}
/** * Populates the roles. This method is a shortcut for calling * {@link #authorities(String...)}, but automatically prefixes each entry with * "ROLE_". This means the following: * * <code> * builder.roles("USER","ADMIN"); * </code> * * is equivalent to * * <code> * builder.authorities("ROLE_USER","ROLE_ADMIN"); * </code> * * <p> * This attribute is required, but can also be populated with * {@link #authorities(String...)}. * </p> * * @param roles the roles for this user (i.e. USER, ADMIN, etc). Cannot be null, * contain null values or start with "ROLE_" * @return the {@link User.UserBuilder} for method chaining (i.e. to populate * additional attributes for this user) */
public MyUser.UserBuilder roles(String... roles) {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(
roles.length);
for (String role : roles) {
Assert.isTrue(!role.startsWith("ROLE_"), role
+ " cannot start with ROLE_ (it is automatically added)");
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
return authorities(authorities);
}
/** * Populates the authorities. This attribute is required. * * @param authorities the authorities for this user. Cannot be null, or contain * null values * @return the {@link User.UserBuilder} for method chaining (i.e. to populate * additional attributes for this user) * @see #roles(String...) */
public MyUser.UserBuilder authorities(GrantedAuthority... authorities) {
return authorities(Arrays.asList(authorities));
}
/** * Populates the authorities. This attribute is required. * * @param authorities the authorities for this user. Cannot be null, or contain * null values * @return the {@link User.UserBuilder} for method chaining (i.e. to populate * additional attributes for this user) * @see #roles(String...) */
public MyUser.UserBuilder authorities(List<? extends GrantedAuthority> authorities) {
this.authorities = new ArrayList<GrantedAuthority>(authorities);
return this;
}
/** * Populates the authorities. This attribute is required. * * @param authorities the authorities for this user (i.e. ROLE_USER, ROLE_ADMIN, * etc). Cannot be null, or contain null values * @return the {@link User.UserBuilder} for method chaining (i.e. to populate * additional attributes for this user) * @see #roles(String...) */
public MyUser.UserBuilder authorities(String... authorities) {
return authorities(AuthorityUtils.createAuthorityList(authorities));
}
/** * Defines if the account is expired or not. Default is false. * * @param accountExpired true if the account is expired, false otherwise * @return the {@link User.UserBuilder} for method chaining (i.e. to populate * additional attributes for this user) */
public MyUser.UserBuilder accountExpired(boolean accountExpired) {
this.accountExpired = accountExpired;
return this;
}
/** * Defines if the account is locked or not. Default is false. * * @param accountLocked true if the account is locked, false otherwise * @return the {@link User.UserBuilder} for method chaining (i.e. to populate * additional attributes for this user) */
public MyUser.UserBuilder accountLocked(boolean accountLocked) {
this.accountLocked = accountLocked;
return this;
}
/** * Defines if the credentials are expired or not. Default is false. * * @param credentialsExpired true if the credentials are expired, false otherwise * @return the {@link User.UserBuilder} for method chaining (i.e. to populate * additional attributes for this user) */
public MyUser.UserBuilder credentialsExpired(boolean credentialsExpired) {
this.credentialsExpired = credentialsExpired;
return this;
}
/** * Defines if the account is disabled or not. Default is false. * * @param disabled true if the account is disabled, false otherwise * @return the {@link User.UserBuilder} for method chaining (i.e. to populate * additional attributes for this user) */
public MyUser.UserBuilder disabled(boolean disabled) {
this.disabled = disabled;
return this;
}
public UserDetails build() {
return new User(username, password, !disabled, !accountExpired,
!credentialsExpired, !accountLocked, authorities);
}
}
}
复制代码
login.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>第一个HTML页面</title>
</head>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
自定义表单验证:
<!--<form name="f" action="/login" method="post">-->
<!-- <form name="f" action="/authentication/form" method="post">-->
<form name="f" action="/authentication/form" method="post">
<br/>
用户名:
<input type="text" name="username" placeholder="name"><br/>
密码:
<input type="password" name="password" placeholder="password"><br/>
<input type="checkbox" name="remember-me" value="true">记住我<br/>
<input name="submit" type="submit" value="提交">
</form>
</body>
</html>
复制代码
LoginController:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import urity.demo.entity.User;
@Controller
public class LoginController {
@RequestMapping("/hello")
public String hello() {
System.out.println("kkkk==");
return "login";
}
@RequestMapping("/success")
public String success(){
return "success";
}
@RequestMapping("/forkl")
public String check(User user){
System.out.println(user);
return "success";
}
@RequestMapping("/user")
public String fuinduser(){
return "user";
}
}
复制代码
user.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
因为用了记住我 因此如今能够直接访问了哦!
</body>
</html>
复制代码
到此咱们来启动项目,首次访问http://localhost:8787/user会须要咱们登陆,这里咱们进行登陆先不勾选记住我:
登陆成功后能够正常访问user,而后咱们关闭浏览器从新打开 访问http://localhost:8787/user会被返回到登陆的页面,这个就是没有任何效果的演示.
而后咱们再次登陆,并勾选记住我:
这里咱们登陆成功后关闭浏览器再打开 仍然能够访问http://localhost:8787/user,并且不须要登陆:
这里浏览器作了以下的事情:
到此,rememberme的功能就完成了