Spring security記住我基本原理:html
登陸的時候,請求發送給過濾器UsernamePasswordAuthenticationFilter,當該過濾器認證成功後,會調用RememberMeService,會生成一個token,將token寫入到瀏覽器cookie,同時RememberMeService裏邊還有個TokenRepository,將token和用戶信息寫入到數據庫中。這樣當用戶再次訪問系統,訪問某一個接口時,會通過一個RememberMeAuthenticationFilter的過濾器,他會讀取cookie中的token,交給RememberService,RememberService會用TokenRepository根據token從數據庫中查是否有記錄,若是有記錄會把用戶名取出來,再調用UserDetailService根據用戶名獲取用戶信息,而後放在SecurityContext裏。java
RememberMeAuthenticationFilter在Spring Security中認證過濾器鏈的倒數第二個過濾器位置,當其餘認證過濾器都無法認證成功的時候,就會調用RememberMeAuthenticationFilter嘗試認證。mysql
實現:web
1,登陸表單加上,SpringSecurity在SpringSessionRememberMeServices類裏定義了一個常量,默認值就是remember-mespring
2,根據上邊的原理圖可知,要配置TokenRepository,把生成的token存進數據庫,這是一個配置bean的配置,放在了BrowserSecurityConfig裏sql
3,在configure裏配置數據庫
4,在BrowserProperties里加上自動登陸時間,把記住我時間作成可配置的apache
//記住我秒數配置
private int rememberMeSeconds = 10;api
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的功能就完成了