Spring security记住我基本原理:
登录的时候,请求发送给过滤器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:
4.0.0 urity demo 0.0.1-SNAPSHOT jar demo Demo project for Spring Boot org.springframework.boot spring-boot-starter-parent 2.0.3.RELEASE UTF-8 UTF-8 1.8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.apache.tomcat.embed tomcat-embed-jasper 8.5.12 javax.servlet javax.servlet-api 3.1.0 javax.servlet jstl 1.2 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-starter-security org.slf4j jcl-over-slf4j 1.7.25 org.projectlombok lombok 1.16.22 org.mybatis.spring.boot mybatis-spring-boot-starter 1.2.0 mysql mysql-connector-java com.alibaba druid 1.0.25 org.springframework.social spring-social-config org.springframework.social spring-social-security org.springframework.social spring-social-web org.springframework.security spring-security-config 5.0.6.RELEASE org.springframework.boot spring-boot-maven-plugin
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;@Configurationpublic 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@Componentpublic 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); ListgrantedAuthorityList = 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 Setauthorities; 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 authorities) { this(username, password, true, true, true, true, authorities); } /** * Construct the User
with the details required by * {@link org.springframework.security.authentication.dao.DaoAuthenticationProvider}. * * @param username the username presented to the *DaoAuthenticationProvider
* @param password the password that should be presented to the *DaoAuthenticationProvider
* @param enabled set totrue
if the user is enabled * @param accountNonExpired set totrue
if the account has not expired * @param credentialsNonExpired set totrue
if the credentials have not * expired * @param accountNonLocked set totrue
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 anull
value was passed either as * a parameter or as an element in theGrantedAuthority
collection */ public MyUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection 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 CollectiongetAuthorities() { 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 sortAuthorities( Collection 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 sortedAuthorities = new TreeSet ( 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 , 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. * * 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
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: * * * builder.roles("USER","ADMIN"); *
* * is equivalent to * ** builder.authorities("ROLE_USER","ROLE_ADMIN"); *
* ** This attribute is required, but can also be populated with * {@link #authorities(String...)}. *
* * @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) { Listauthorities = new ArrayList ( 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 authorities) { this.authorities = new ArrayList (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:
第一个HTML页面 Title 自定义表单验证:
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;@Controllerpublic 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:
Title 由于用了记住我 所以现在可以直接访问了哦!
到此我们来启动项目,首次访问http://localhost:8787/user会需要我们登录,这里我们进行登录先不勾选记住我:
登录成功后可以正常访问user,然后我们关闭浏览器重新打开 访问http://localhost:8787/user会被返回到登录的页面,这个就是没有任何效果的演示.
然后我们再次登录,并勾选记住我:
这里我们登录成功后关闭浏览器再打开 仍然可以访问http://localhost:8787/user,而且不需要登录:
这里浏览器做了如下的事情:
- 在我们数据库建立表并插入数据
- 然后我们关闭浏览器在访问,它会去库里面查找响应的token,如果有就不用登录直接访问:
到此,rememberme的功能就完成了