spring-security使用-登錄(一)


Form表單登錄

默認登錄

1.pom配置

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

2.java類

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

3.啟動項目

可以發現日志打印了了密碼默認是user用戶

 

3.訪問

localhost:8080/hello 重定向到了http://localhost:8080/login

 

3.輸入用戶名密碼再訪問

用戶名:user 密碼:7e6a1360-1115-4eb6-8e17-5308164e5b26

 

4.默認生成的生成的用戶名和密碼如何生成

查看UserDetailsServiceAutoConfiguration此類

org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration#getOrDeducePassword

 private String getOrDeducePassword(User user, PasswordEncoder encoder) {
     //user獲得密碼
String password
= user.getPassword(); if (user.isPasswordGenerated()) {
//這里就是控制台打印的日志 logger.info(String.format(
"%n%nUsing generated security password: %s%n", user.getPassword())); } return encoder == null && !PASSWORD_ALGORITHM_PATTERN.matcher(password).matches() ? "{noop}" + password : password; }

5.我們再看User

這里一目了然 默認是user和uuid生成用戶名和密碼,我們可以通過在配置文件配置修改

spring.security.user.name=liqiang
spring.security.user.password=liqiang
@ConfigurationProperties(
        prefix = "spring.security"//說明我們可以通過配置文件配置密碼和user
)
public class SecurityProperties {
    private SecurityProperties.User user = new SecurityProperties.User();

    public static class User {
        //用戶 默認user
        private String name = "user";
        //使用uuid生成密碼
        private String password = UUID.randomUUID().toString();
        private List<String> roles = new ArrayList();
        private boolean passwordGenerated = true;

        public void setPassword(String password) {
            //密碼是否不為空
            if (StringUtils.hasLength(password)) {
                //標簽打為false  后面if (user.isPasswordGenerated()) 打印日志
                this.passwordGenerated = false;
                this.password = password;
            }
        }
    }
}

內存中多用戶配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 對密碼進行加密的實例
     * @return
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        /**
         * 不加密所以使用NoOpPasswordEncoder
         * 更多可以參考PasswordEncoder 的默認實現官方推薦使用: BCryptPasswordEncoder,BCryptPasswordEncoder
         */
        return NoOpPasswordEncoder.getInstance();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        /**
         *  inMemoryAuthentication 開啟在內存中定義用戶
         *  多個用戶通過and隔開
         */
        auth.inMemoryAuthentication()
                .withUser("liqiang").password("liqiang").roles("admin")
                 .and()
                .withUser("admin").password("admin").roles("admin");
    }
}
PasswordEncoder為防止密碼泄露,數據庫保存的是加密的密碼,然后前端登錄傳過來加密后根據數據庫的進行匹配 我們可以自己實現和用默認的

自定義登錄頁面

security的默認登錄頁面不能滿足我們需求,我們大多數場景都需要自定義登錄頁面

1.增加自定義登錄頁面html

 

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--會get請求login.html 提交則是post login--> <form action="/login.html" method="post"> <div class="input"> <label for="name">用戶名</label> <input type="text" name="username" id="name"> <span class="spin"></span> </div> <div class="input"> <label for="pass">密碼</label> <input type="password" name="password" id="pass"> <span class="spin"></span> </div> <div class="button login"> <button type="submit"> <span>登錄</span> <i class="fa fa-check"></i> </button> </div> </form> </body> </html>

2.配置自定義html頁面路徑

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 對於不需要授權的靜態文件放行
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/js/**", "/css/**", "/images/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()//form表單的方式
                .loginPage("/login.html")//登錄頁面路徑
                .permitAll()//不攔截
                .and()
                .csrf()//記得關閉
                .disable();
    }
}@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 對於不需要授權的靜態文件放行
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/js/**", "/css/**", "/images/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()//form表單的方式
                .loginPage("/login.html")//登錄頁面路徑
                .permitAll()//不攔截
                .and()
                .csrf()//記得關閉
                .disable();
    }
}

 

3.再次訪問就是顯示自定義的登錄頁面

自定義登錄請求地址

1.如果我們引入security 什么都不做那么spring securty的默認的登錄頁面和登錄地址為以下,我們可以手動修改

get http://localhost:8080/login  登錄頁面

post  http://localhost:8080/login 登錄請求

我們前面自定義了登錄頁面請求但是請求地址action配置的是否可以自定義為其他

<form action="/login.html" method="post">

2.我們可以通過后台配置

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")//登錄頁面路徑
                .loginProcessingUrl("/doLogin")
                .permitAll()//不攔截
                .and()
                .csrf()//記得關閉
                .disable();
    }

我們的form表單action配置就可以改為

<form action="/doLogin" method="post">

登錄url自定義和處理登錄的源碼處

默認配置是在哪里配置的呢可以看org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer

    public FormLoginConfigurer() {
//<1>調用父類的 詳情看里面
super(new UsernamePasswordAuthenticationFilter(), (String)null);
//用戶名密碼默認參數名
this.usernameParameter("username"); this.passwordParameter("password"); }

<1>org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer#AbstractAuthenticationFilterConfigurer()

protected AbstractAuthenticationFilterConfigurer(F authenticationFilter, String defaultLoginProcessingUrl) {
        
<2>
this(); this.authFilter = authenticationFilter; if (defaultLoginProcessingUrl != null) { this.loginProcessingUrl(defaultLoginProcessingUrl); } }

<2>org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer.AbstractAuthenticationFilterConfigurer

  protected AbstractAuthenticationFilterConfigurer() {
        this.defaultSuccessHandler = new SavedRequestAwareAuthenticationSuccessHandler();
        this.successHandler = this.defaultSuccessHandler;
//loginPage的默認值是/login
this.setLoginPage("/login"); }

4.默認登錄請求路徑的源碼處

org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer#init

 public void init(H http) throws Exception {
//<1>調用了父類的init
super.init(http); this.initDefaultLoginFilter(http); }

<1>org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer#init

    public void init(B http) throws Exception {
<2>
this.updateAuthenticationDefaults(); this.updateAccessDefaults(http); this.registerDefaultAuthenticationEntryPoint(http); }

<2>

org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer#updateAuthenticationDefaults

protected final void updateAuthenticationDefaults() {
       //可以看到本質是使用使用loginProcessingUrl,如果我們沒有手動設置才使用默認的loginPage
        if (this.loginProcessingUrl == null) {
            this.loginProcessingUrl(this.loginPage);
        }

        if (this.failureHandler == null) {
            this.failureUrl(this.loginPage + "?error");
        }

        LogoutConfigurer<B> logoutConfigurer = (LogoutConfigurer)((HttpSecurityBuilder)this.getBuilder()).getConfigurer(LogoutConfigurer.class);
        if (logoutConfigurer != null && !logoutConfigurer.isCustomLogoutSuccess()) {
            logoutConfigurer.logoutSuccessUrl(this.loginPage + "?logout");
        }

    }

自定義登錄用戶名密碼參數

1.默認情況下 用戶名為username 密碼為password為必須

  <div class="input">
        <label for="name">用戶名</label>
        <input type="text" name="username" id="name">
        <span class="spin"></span>
    </div>
    <div class="input">
        <label for="pass">密碼</label>
        <input type="password" name="password" id="pass">
        <span class="spin"></span>
    </div>

2.我們可以通過配置修改

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")//登錄頁面路徑
                .loginProcessingUrl("/doLogin")
                .usernameParameter("loginName")
                .passwordParameter("loginPassword")
                .permitAll()//不攔截
                .and()
                .csrf()//記得關閉
                .disable();
    }
}

自定義登錄用戶名密碼參數源碼處

org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer

public FormLoginConfigurer() {
        super(new UsernamePasswordAuthenticationFilter(), (String)null);
//設置參數名字
this.usernameParameter("username"); this.passwordParameter("password"); }

 

2.可以發現最終是調用UsernamePasswordAuthenticationFilter對象的set方法 所以我們可以通過build的時候直接調用覆蓋

org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer#usernameParameter

org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer#passwordParameter

 public FormLoginConfigurer<H> usernameParameter(String usernameParameter) {
((UsernamePasswordAuthenticationFilter)
this.getAuthenticationFilter()).setUsernameParameter(usernameParameter); return this; } public FormLoginConfigurer<H> passwordParameter(String passwordParameter) { ((UsernamePasswordAuthenticationFilter)this.getAuthenticationFilter()).setPasswordParameter(passwordParameter); return this; }

3.fitler內部獲取用戶名密碼根據配置的usernameParameter和passwordParameter獲取用戶名密碼

org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
//<1> String username
= this.obtainUsername(request); String password = this.obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } }

<1>

 @Nullable
    protected String obtainPassword(HttpServletRequest request) {
//通過我們配置的名字獲取
return request.getParameter(this.passwordParameter); } @Nullable protected String obtainUsername(HttpServletRequest request) {
//通過我們配置的名字獲取
return request.getParameter(this.usernameParameter); }

自定義登錄跳轉

1.security可通過defaultSuccessUrl、successForwardUrl指定登錄成功跳轉頁面

defaultSuccessUrl 如果是通過訪問其他頁面無權訪問重定向到登錄頁面登錄,登錄成功則跳轉到來源頁面,如果是直接訪問登錄頁面則直接跳轉到指定url

successForwardUrl 登錄成功不管來源直接跳轉到指定頁面 (注意 這接口采用的服務單轉發的方式而不是302重定向)

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")//登錄頁面路徑
                .loginProcessingUrl("/doLogin")
                .usernameParameter("loginName")
                .passwordParameter("loginPassword")
                .defaultSuccessUrl("/index")
                .successForwardUrl("/index")
                .permitAll()//不攔截
                .and()
                .csrf()//記得關閉
                .disable();
    }

自定義登錄失敗跳轉

failureForwardUrl 登錄失敗服務器重定向

failureUrl 登錄失敗前端302重定向地址

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")//登錄頁面路徑
                .loginProcessingUrl("/doLogin")
                .usernameParameter("loginName")
                .passwordParameter("loginPassword")
                .defaultSuccessUrl("/index")
                .successForwardUrl("index")
                .failureForwardUrl("/loginFail")
                .failureUrl("/login.html")
                .permitAll()//不攔截
                .and()
                .csrf()//記得關閉
                .disable();
    }
}

注銷登錄

.and()
.logout()
.logoutUrl("/logout")//自定義注銷地址
.logoutRequestMatcher(new AntPathRequestMatcher("/logout","POST"))  //自定義請求方式
.logoutSuccessUrl("/login.html") //注銷后跳轉頁面
.deleteCookies()//清除cookie
.clearAuthentication(true)//清除權限相關
.invalidateHttpSession(true)//清除session
.permitAll()
.and()

ajax登錄+驗證碼登錄

1.自定義登錄處理器

public class CustomizeUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        //從session獲得驗證碼
        String verify_code = (String) request.getSession().getAttribute("verify_code");
        //判斷是否是json post請求 如果不是則走父類的form登錄
        if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
            Map<String, String> loginData = new HashMap<>();
            try {
                //解析bodyjson數據轉為Map
                loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
            } catch (IOException e) {
                throw new AuthenticationServiceException("系統異常");
            }
            String code = loginData.get("code");
            //檢查驗證碼
            checkCode(response, code, verify_code);
            //獲得用戶輸入的用戶名和密碼 如果是form登錄的話上面配置的用戶名key和密碼key就不會起效 用默認的
            String username = loginData.get(getUsernameParameter());
            String password = loginData.get(getPasswordParameter());
            if (username == null) {
                username = "";
            }
            if (password == null) {
                password = "";
            }
            username = username.trim();
            //模擬父類的實現
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    username, password);
            setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        } else {
            checkCode(response, request.getParameter("code"), verify_code);
            //如果是form登錄直接使用父類的
            return super.attemptAuthentication(request, response);
        }
    }
    public void checkCode(HttpServletResponse resp, String code, String verify_code) {
        if (code == null || verify_code == null || "".equals(code) || !verify_code.toLowerCase().equals(code.toLowerCase())) {
            //驗證碼不正確
            throw new AuthenticationServiceException("驗證碼不正確");
        }
    }
}

2.初始化登錄處理器並重寫登錄成功和登錄失敗的邏輯

com.liqiang.demo.configs.SecurityConfig#initLoginFilter

 public Filter initLoginFilter() throws Exception {
        CustomizeUsernamePasswordAuthenticationFilter customizeUsernamePasswordAuthenticationFilter= new CustomizeUsernamePasswordAuthenticationFilter();
        /**
         * 授權成功處理器 可以看父類里面默認就是這2個
         */
        SavedRequestAwareAuthenticationSuccessHandler handler=new SavedRequestAwareAuthenticationSuccessHandler();
        AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
        customizeUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                //此處我們做了兼容 如果是json 則響應json 否則走form默認的邏輯
                if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
                    response.setContentType("application/json;charset=utf-8");
                    PrintWriter out = response.getWriter();
                    Map<String,Object> result=new HashMap<>();
                    result.put("code",200);
                    result.put("message","登錄成功");
                    String s = new ObjectMapper().writeValueAsString(result);
                    out.write(s);
                    out.flush();
                    out.close();
                }else{
                    //表單登錄委托給默認的處理器
                    handler.onAuthenticationSuccess(request,response,authentication);
                }
            }
        });
        //授權失敗處理器
        customizeUsernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                //此處我們做了兼容 如果是json 則響應json 否則走form默認的邏輯
                if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
                    response.setContentType("application/json;charset=utf-8");
                    PrintWriter out = response.getWriter();
                    Map<String,Object> result=new HashMap<>();
                    result.put("code",200);
                    result.put("message","登錄失敗");
                    if (exception instanceof LockedException) {
                        result.put("code",901);
                        result.put("message","賬戶被鎖定,請聯系管理員!");
                    } else if (exception instanceof CredentialsExpiredException) {
                        result.put("code",902);
                        result.put("message","密碼過期,請聯系管理員!");
                    } else if (exception instanceof AccountExpiredException) {
                        result.put("code",903);
                        result.put("message","賬戶過期,請聯系管理員!");
                    } else if (exception instanceof DisabledException) {

                        result.put("code",904);
                        result.put("message","賬戶被禁用,請聯系管理員!");
                    } else if (exception instanceof BadCredentialsException) {
                        result.put("code",905);
                        result.put("message","用戶名或者密碼輸入錯誤,請重新輸入!");
                    }
                    out.write(new ObjectMapper().writeValueAsString(result));
                    out.flush();
                    out.close();
                }else{
                    //表單登錄委托給默認的處理器
                    failureHandler.onAuthenticationFailure(request,response,exception);
                }
            }
        });
        customizeUsernamePasswordAuthenticationFilter.setAuthenticationManager(super.authenticationManagerBean());
        //攔截處理的url
        customizeUsernamePasswordAuthenticationFilter.setFilterProcessesUrl("/doLogin");
        return customizeUsernamePasswordAuthenticationFilter;
    }

3.替換默認的登錄處理器

   @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")//登錄頁面路徑
                .loginProcessingUrl("/doLogin")
                .usernameParameter("loginName")
                .passwordParameter("loginPassword")
                .defaultSuccessUrl("/index")
                .successForwardUrl("/index")
                .failureForwardUrl("/loginFail")
                .failureUrl("/login.html")
                .permitAll()//不攔截
                .and()
                //替換默認的登錄處理器 注意form的配置將不起效果 可看formLogin源碼 並沒有替換 比如 usernameParameter passwordParameter
                .addFilterAt(initLoginFilter(), UsernamePasswordAuthenticationFilter.class)
                .csrf()//記得關閉
                .disable();
    }

 

 

4.測試

自動登錄 

簡單實用

1.在原來的基礎上增加記住密碼配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 對於不需要授權的靜態文件放行
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/js/**", "/css/**", "/images/**");
    }
    /**
     * 對密碼進行加密的實例
     * @return
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        /**
         * 不加密所以使用NoOpPasswordEncoder
         * 更多可以參考PasswordEncoder 的默認實現官方推薦使用: BCryptPasswordEncoder,BCryptPasswordEncoder
         */
        return NoOpPasswordEncoder.getInstance();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        /**
         *  inMemoryAuthentication 開啟在內存中定義用戶
         *  多個用戶通過and隔開
         */
        auth.inMemoryAuthentication()
                .withUser("liqiang").password("liqiang").roles("admin")
                .and()
                .withUser("admin").password("admin").roles("admin");
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and().rememberMe()//記住登錄
                .and()
                .formLogin()//form表單的方式
                .loginPage("/login.html")//登錄頁面路徑
                .loginProcessingUrl("/doLogin") //自定義登錄請求地址
                .defaultSuccessUrl("/hello") //登錄無權限訪問頁面默認調整頁面
                .usernameParameter("loginName")
                .passwordParameter("loginPassword")
                .permitAll()//不攔截
                .and()
                .csrf()//記得關閉
                .disable();
    }
}

 

 

 

原理

1.在登錄時發現提交參數增加了一個remember-me: on

 

 

2.退出瀏覽器都會默認帶上cookie

Cookie:
JSESSIONID=5401CED03129ED6CF941CF9812170EAE; remember-me=bGlxaWFuZzoxNjEwMDk0MzgyMDQ2OjVlODZkOTc2OTY0M2UzMmZlZjUyNTgzYWU0NjhhNTJh

3.將remember-me的值通過base 64解密發現

值為:  liqiang:1610094382046:5e86d9769643e32fef52583ae468a52a

第一個為用戶名 第二個為過期時間 第三個為加密key 

4.remomber-me生成源碼

org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices

 public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        //從登錄Authentication 獲取用戶名和密碼
        String username = this.retrieveUserName(successfulAuthentication);
        String password = this.retrievePassword(successfulAuthentication);
        if (!StringUtils.hasLength(username)) {
            this.logger.debug("Unable to retrieve username");
        } else {
            //因為登錄成功密碼有可能被其他filter擦除 這里如果沒有再查一次
            if (!StringUtils.hasLength(password)) {
                UserDetails user = this.getUserDetailsService().loadUserByUsername(username);
                password = user.getPassword();
                if (!StringUtils.hasLength(password)) {
                    this.logger.debug("Unable to obtain password for user: " + username);
                    return;
                }
            }
            //獲取過期時間秒默認是 應該是可配的  默認是1209600
            int tokenLifetime = this.calculateLoginLifetime(request, successfulAuthentication);
            long expiryTime = System.currentTimeMillis();
            //當前時間加上過期時間毫秒
            expiryTime += 1000L * (long)(tokenLifetime < 0 ? 1209600 : tokenLifetime);
            //算出加密值
            String signatureValue = this.makeTokenSignature(expiryTime, username, password);
            // 用戶名+:+過期時間+:密碼+:+加密值 寫入cookie 並設置過期時間
            this.setCookie(new String[]{username, Long.toString(expiryTime), signatureValue}, tokenLifetime, request, response);
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Added remember-me cookie for user '" + username + "', expiry: '" + new Date(expiryTime) + "'");
            }

        }
    }
    protected String makeTokenSignature(long tokenExpiryTime, String username, String password) {
        //用戶名+:+過期時間+:密碼+:秘鑰 這個getKey是uuid  每次重啟 都需要重新登錄 我們可以配置寫死
String data = username + ":" + tokenExpiryTime + ":" + password + ":" + this.getKey(); MessageDigest digest; try { //進行md5加密 digest = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException var8) { throw new IllegalStateException("No MD5 algorithm available!"); } return new String(Hex.encode(digest.digest(data.getBytes()))); }

key配置

 protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and().rememberMe()
            .key("system")
                .and()
                .formLogin()
//                .loginPage("/login.html")//登錄頁面路徑
//                .loginProcessingUrl("/doLogin")
                .usernameParameter("loginName")
                .passwordParameter("loginPassword")
                .defaultSuccessUrl("/hello")
                .failureForwardUrl("/loginFail")
                .failureUrl("/login.html")
                .permitAll()//不攔截
                .and()
                //替換默認的登錄處理器 注意順序 因為下面有給攔截器的屬性賦值 比如 usernameParameter passwordParameter
                .addFilterAt(initLoginFilter(), UsernamePasswordAuthenticationFilter.class)
                .csrf()//記得關閉
                .disable();
    }

 

5.校驗

org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter

  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 取出cookie 進行解析校驗有消息
            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);
        }

    }

持久化令牌

簡單使用

配合記住登錄使用

可以通過redis和數據庫存儲token

  1. 避免使用內存session,一旦用戶量大了,內存極有可能爆掉。
  2. 避免使用內存session,重啟后需要重新登錄。
  3. 分布式情況下應用存儲 別的地方獲取不到token

默認實現有2種 InMemoryTokenRepositoryImpl 內存 或者JdbcTokenRepositoryImpl數據庫,表結構可以參考impl里面的crud 手動創建

參考的org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl 實現 由查詢數據庫改為查redis 

@Component
public class RedisPersistentTokenRepository implements PersistentTokenRepository {
    TokenRedisRepository tokenRedisRepository;
    PersistentRememberMeTokenConvert persistentRememberMeTokenConvert;
    @Override
    public void createNewToken(PersistentRememberMeToken persistentRememberMeToken) {
        PersistentRememberMeTokenRo persistentRememberMeTokenRo=persistentRememberMeTokenConvert.toRo(persistentRememberMeToken);
        //保存到redis hash方式 key為token:{series}
        tokenRedisRepository.save(persistentRememberMeToken);
    }

    @Override
    public void updateToken(String series, String tokenValue, Date lastUsed) {
        PersistentRememberMeTokenRo persistentRememberMeTokenRo=tokenRedisRepository.findOne(series);
        persistentRememberMeTokenRo.setDate(lastUsed);
        persistentRememberMeTokenRo.setTokenValue(tokenValue);
        //保存到redis hash方式 key為token:{userName}
        tokenRedisRepository.save(persistentRememberMeToken);
    }

    @Override
    public PersistentRememberMeToken getTokenForSeries(String series) {
        PersistentRememberMeTokenRo persistentRememberMeTokenRo=tokenRedisRepository.findOne(series);
        return persistentRememberMeTokenConvert.toPersistentRememberMeToken(persistentRememberMeTokenRo);;
    }

    @Override
    public void removeUserTokens(String series) {
        tokenRedisRepository.del(s);
    }
}

配置

 http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .rememberMe()
                .tokenRepository(getApplicationContext().getBean(RedisPersistentTokenRepository.class))
                .key("system")
                .and()
                .formLogin()
//                .loginPage("/login.html")//登錄頁面路徑
//                .loginProcessingUrl("/doLogin")
                .usernameParameter("loginName")
                .passwordParameter("loginPassword")
                .defaultSuccessUrl("/hello")
                .failureForwardUrl("/loginFail")
                .failureUrl("/login.html")
                .permitAll()//不攔截
                .and()
                //替換默認的登錄處理器 注意順序 因為下面有給攔截器的屬性賦值 比如 usernameParameter passwordParameter
                .addFilterAt(initLoginFilter(), UsernamePasswordAuthenticationFilter.class)
                .csrf()//記得關閉
                .disable();

源碼

1.寫入

org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices

  protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        String username = successfulAuthentication.getName();
        this.logger.debug("Creating new persistent login for user " + username);
        //創建persistentTokend對象
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());
        try {
            //持久化
            this.tokenRepository.createNewToken(persistentToken);
            //寫入cookie
            this.addCookie(persistentToken, request, response);
        } catch (Exception var7) {
            this.logger.error("Failed to save persistent token ", var7);
        }

    }

2.校驗

重寫了父類的此方法

org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices#processAutoLoginCookie

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM