登录的时候,请求发送给过滤器UsernamePasswordAuthenticationFilter,当该过滤器认证成功后,会调用RememberMeService,会生成一个token,将token写入到浏览器cookie,同时RememberMeService里边还有个TokenRepository,将token和用户信息写入到数据库中。这样当用户再次访问系统,访问某一个接口时,会经过一个RememberMeAuthenticationFilter的过滤器,他会读取cookie中的token,交给RememberService,RememberService会用TokenRepository根据token从数据库中查是否有记录,如果有记录会把用户名取出来,再调用UserDetailService根据用户名获取用户信息,然后放在SecurityContext里。
RememberMeAuthenticationFilter在Spring Security中认证过滤器链的倒数第二个过滤器位置,当其他认证过滤器都没法认证成功的时候,就会调用RememberMeAuthenticationFilter尝试认证。
实现:
1,登录表单加上 ,SpringSecurity在SpringSessionRememberMeServices类里定义了一个常量,默认值就是remember-me
2,根据上边的原理图可知, 要配置TokenRepository, 把生成的token存进数据库,这是一个配置bean的配置,放在了 BrowserSecurityConfig里
3,在configure里配置
4,在BrowserProperties里加上自动登录时间,把记住我时间做成可配置的
//记住我秒数配置 private int rememberMeSeconds = 10;
pom.xml:
<?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:
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的功能就完成了