1. 程式人生 > >spring-security-oauth2(二) 自定義個性化登入

spring-security-oauth2(二) 自定義個性化登入

自定義認證邏輯

1.認證邏輯介面

spring-security使用者登入邏輯驗證介面org.springframework.security.core.userdetails.UserDetailsService只有一個方法

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

UserDetail資訊如下:我們自定義的使用者資訊要實現這個介面,

public interface UserDetails extends Serializable {

    //許可權相關
    Collection<? extends GrantedAuthority> getAuthorities();
    //獲取密碼
    String getPassword();
    //獲取使用者名稱
    String getUsername();
    //賬戶是否驗證過期
    boolean isAccountNonExpired();
    //賬戶是否鎖定
    boolean isAccountNonLocked();
     //賬戶驗證是否過期
    boolean isCredentialsNonExpired();
    //賬戶是否有效
    boolean isEnabled();
}

org.springframework.security.core.userdetails.User這個是它的一個實現

this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));//執行緒安全的許可權新增 同時有內部類自定義排序

2.處理密碼加密解密

配置了這個Bean以後,從前端傳遞過來的密碼就會被加密,所以從資料庫查詢到的密碼必須是經過加密的,而這個過程都是在使用者註冊的時候進行加密的。這就合理解釋了為什麼對上面的程式碼進行加密了。

org.springframework.security.crypto.password.PasswordEncoder

public interface PasswordEncoder {
    //加密
    String encode(CharSequence var1);
    //驗證是否匹配
    boolean matches(CharSequence var1, String var2);
}

 在瀏覽器許可權配置類BrowserSecurityConfig中注入這個bean

/**
 * 瀏覽器security配置類
 *
 * @author CaiRui
 * @date 2018-12-4 8:41
 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 密碼加密解密
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.
                //spring5後預設就是表單登入方式
                // httpBasic().
                        formLogin().
                and().
                authorizeRequests().
                anyRequest().
                authenticated();
    }

}

3.自定義介面實現

package com.rui.tiger.auth.browser.user;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
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;

/**
 * 自定義使用者登入實現
 *
 * @author CaiRui
 * @date 2018-12-5 8:19
 */
@Component
@Slf4j
public class MyUserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //TODO 後續做成資料庫實現(MyBaites-plus實現)先實現流程
        //1.根據使用者名稱去資料庫去查詢使用者資訊獲取加密後的密碼 這裡模擬一個加密的資料庫密碼
        String encryptedPassWord = passwordEncoder.encode("123456");
        log.info("模擬加密後的資料庫密碼:{}",encryptedPassWord);
        //2.這裡可以去驗證賬戶的其它相關資訊 預設都通過
        
        //3.返回認證過的使用者資訊  授予一個admin的許可權
        return new User(username,
                encryptedPassWord,
                true,
                true,
                true,
                true,
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

實現完了我們啟動專案來驗證下配置的MyUserDetailServiceImpl是否成功了,可以看到預設的隨機密碼在控制檯已經沒有了。瀏覽器隨便訪問一個地址,會調到預設的登入表單介面

密碼我們先隨便輸入一個 比如66666

可以看到登入失敗,我們再輸入我們固定的密碼123456

 可以看到我們登入成功,所以出現這個介面是因為http://localhost:8070/user這個我沒有實現,驗證成功後重定向到之前的地址   同時我們可以看到控制檯也會列印如下資訊 證明我們的自定義認證成功。ok下面我們開始實現自己的個性化登入需求開發

 

4.個性化登入實現

在實際開發中通常我們都不會使用spring-security預設的登入介面,我們可以通過配置實現自己的個性化登入,下面是具體實現。

1)自定義登入頁面

首先修改我們的瀏覽器配置類BrowserSecurityConfig,同時要在資原始檔下新增我們的自定義登入介面/tiger-login.html

package com.rui.tiger.auth.browser.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 瀏覽器security配置類
 *
 * @author CaiRui
 * @date 2018-12-4 8:41
 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 密碼加密解密
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .formLogin()
                .loginPage("/tiger-login.html")//自定義標準登入介面
                .and()
                .authorizeRequests()
                .antMatchers("/tiger-login.html")//此路徑放行 否則會陷入死迴圈
                .permitAll()
                .anyRequest()
                .authenticated();
    }

}

tiger-login.html檔案如下,注意放置的路徑

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>標準登入頁面</title>
</head>
<body>
<h2>標準登入頁面</h2>
<h3>表單登入</h3>
<form action="/authentication/form" method="post">
    <table>
        <tr>
            <td>使用者名稱:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>密碼:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit">登入</button>
            </td>
        </tr>
    </table>
</form>
</body>
</html>

ok 我們來啟動專案輸入http://localhost:8070/user  看看效果,可以看見已經成功跳到我們的自定義介面了

 我們再次輸入使用者名稱user和密碼123456試試看

可以看見又重定向到我們的tiger-login.html,這是怎麼回事呢?

原來是是我們的 tiger-login.html定義的表單請求<form action="/authentication/form" method="post">和spring-security預設的表單登入請求不一致,參見UsernamePasswordAuthenticationFilter原始碼如下:

public UsernamePasswordAuthenticationFilter() {
   super(new AntPathRequestMatcher("/login", "POST"));
}

我們只要BrowserSecurityConfig新增自定義表單的請求路徑就可以loginProcessingUrl("/authentication/form"),同時進行許可權放行,並關閉跨域訪問,相關配置如下

package com.rui.tiger.auth.browser.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 瀏覽器security配置類
 *
 * @author CaiRui
 * @date 2018-12-4 8:41
 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 密碼加密解密
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .formLogin()
                .loginPage("/tiger-login.html")//自定義標準登入介面
                .loginProcessingUrl("/authentication/form")//自定義表單請求路徑
                .and()
                .authorizeRequests()
                .antMatchers("/tiger-login.html","/authentication/form")//此路徑放行 否則會陷入死迴圈
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .csrf().disable()//跨域關閉
        ;
    }

}

再次訪問 localhost:8070/user/hello  可以看到api可以成功訪問了

這裡雖然配置了自定義的路徑,但都是統一跳轉到了靜態介面,在現在流行的前後臺分離的專案中,返回給前臺的通常都是一個json串,那麼要怎麼實現 根據請求來分發是返回html內容?還是返回json內容呢? 

處理不同型別的請求

由於我們程式中有很多資訊來自配置檔案,下面我們用類來統一管理請看下面實現,先看下他們的關係

SecurityPropertie 許可權配置父類

          BrowserProperties 瀏覽器相關配置

          AppProperties   移動端相關配置

          SocialProperties 社交相關配置

         CaptchaProperties 驗證碼相關配置

         。。。。。。。。。。  

由於這些配置類是browser和app專案公用的,所以寫在核心模組core裡 

package com.rui.tiger.auth.core.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * 許可權配置檔案父類(注意這裡不用lombok 會讀取不到)
 * 這裡會有很多許可權配置子模組
 * @author CaiRui
 * @date 2018-12-6 8:41
 */

@ConfigurationProperties(value = "tiger.auth",ignoreInvalidFields = true)
public class SecurityProperties {

    /**
     * 瀏覽器配置類
     */
    private BrowserProperties browser = new BrowserProperties();

    public BrowserProperties getBrowser() {
        return browser;
    }

    public void setBrowser(BrowserProperties browser) {
        this.browser = browser;
    }
}

 BrowserProperties 瀏覽器配置如下:

package com.rui.tiger.auth.core.properties;

import com.rui.tiger.auth.core.model.enums.LoginTypeEnum;

/**
 * 瀏覽器配置
 *
 * @author CaiRui
 * @date 2018-12-6 8:42
 */
public class BrowserProperties {
    /**
     * 登入頁面 不配置預設標準登入介面
     */
    private String loginPage = "/tiger-login.html";
    /**
     * 跳轉型別 預設返回json資料
     */
    private LoginTypeEnum loginType = LoginTypeEnum.JSON;


    public String getLoginPage() {
        return loginPage;
    }

    public void setLoginPage(String loginPage) {
        this.loginPage = loginPage;
    }

    public LoginTypeEnum getLoginType() {
        return loginType;
    }

    public void setLoginType(LoginTypeEnum loginType) {
        this.loginType = loginType;
    }
}

還要一個配置類SecurityPropertiesCoreConfig來使上面的配置生效 

package com.rui.tiger.auth.core.config;

import com.rui.tiger.auth.core.properties.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * SecurityProperties 配置類注入生效
 *
 * @author CaiRui
 * @date 2018-12-6 8:57
 */
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityPropertiesCoreConfig {
    
}

專案application.yml配置檔案如下配置

spring:
  datasource:
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://my.yunout.com:3306/tiger_study?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8
    username: root
    password: root
    # 配置Druid連線池
    type: com.alibaba.druid.pool.DruidDataSource
  session:
    store-type: none

# Tomcat
server:
  port: 8070
  connection-timeout: 5000ms
#自定義許可權配置
tiger:
  auth:
     browser:
      #loginPage: /demo-login.html # 這裡可以配置成自己的非標準登入介面
      loginType: JSON

LoginTypeEnum是BrowserProperties中控制跳轉行為的列舉類

package com.rui.tiger.auth.core.model.enums;

import lombok.Getter;

/**
 * 登入型別列舉類
 * @author CaiRui
 * @date 2018-12-6 12:45
 */
@Getter
public enum LoginTypeEnum {

    /**
     * json資料返回
     */
    JSON,
    /**
     * 重定向
     */
    REDIRECT;
}

ok 上面許可權配置類都準備完成了,修改瀏覽器配置類,使其登入路徑是我們自定義的控制器路徑,裡面控制是返回josn 還是html介面,

同時裡面還要我們自定義的登入成功和失敗處理器,這個我們稍後來說。

package com.rui.tiger.auth.browser.config;

import com.rui.tiger.auth.core.authentication.TigerAuthenticationFailureHandler;
import com.rui.tiger.auth.core.authentication.TigerAuthenticationSuccessHandler;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 瀏覽器security配置類
 *
 * @author CaiRui
 * @date 2018-12-4 8:41
 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private SecurityProperties securityProperties;
    @Autowired
    private TigerAuthenticationFailureHandler tigerAuthenticationFailureHandler;
    @Autowired
    private TigerAuthenticationSuccessHandler tigerAuthenticationSuccessHandler;

    /**
     * 密碼加密解密
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .formLogin()
                .loginPage( "/authentication/require")//自定義登入請求
                .loginProcessingUrl("/authentication/form")//自定義表單登入地址
                .successHandler(tigerAuthenticationSuccessHandler)
                .failureHandler(tigerAuthenticationFailureHandler)
                .and()
                .authorizeRequests()
                .antMatchers(securityProperties.getBrowser().getLoginPage(),
                        "/authentication/require")//此路徑放行 否則會陷入死迴圈
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .csrf().disable()//跨域關閉
        ;
    }

}

編寫處理請求的處理器BrowserRequireController

package com.rui.tiger.auth.browser.controller;

import com.rui.tiger.auth.core.properties.SecurityProperties;
import com.rui.tiger.auth.core.support.SimpleResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 使用者登入認證控制器
 *
 * @author CaiRui
 * @date 2018-12-5 12:44
 */
@RestController
@Slf4j
public class BrowserRequireController {

    //封裝了引發跳轉請求的工具類  https://blog.csdn.net/honghailiang888/article/details/53671108
    private RequestCache requestCache = new HttpSessionRequestCache();
    // spring的工具類:封裝了所有跳轉行為策略類
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Autowired
    private SecurityProperties securityProperties;

    private static final String HTML_SUFFIX = ".html";



    /**
     * 當需要進行身份認證的時候跳轉到此方法
     *
     * @param request  請求
     * @param response 響應
     * @return 將資訊以JSON形式返回給前端
     */
    @RequestMapping("/authentication/require")
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
        log.info("BrowserRequireController進來了 啦啦啦");
        // 從session快取中獲取引發跳轉的請求
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if (null != savedRequest) {
            String redirectUrl = savedRequest.getRedirectUrl();
            log.info("引發跳轉的請求是:{}", redirectUrl);
            if (StringUtils.endsWithIgnoreCase(redirectUrl, HTML_SUFFIX)) {
                // 如果是HTML請求,那麼就直接跳轉到HTML,不再執行後面的程式碼
                redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
            }
        }
        return new SimpleResponse("訪問的服務需要身份認證,請引導使用者到登入頁面");
    }


}

同時編寫我們的登入成功TigerAuthenticationSuccessHandler和失敗處理器TigerAuthenticationFailureHandler,這裡可以加入我們的一些邏輯 比如登入成功記錄日誌,這裡只是返回json還是重定向處理,通過配置 BrowserProperties中的loginType就可以實現,參看上面。

TigerAuthenticationSuccessHandler

package com.rui.tiger.auth.core.authentication;

import com.alibaba.fastjson.JSON;
import com.rui.tiger.auth.core.model.enums.LoginTypeEnum;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 認證成功處理器
 * {@link SavedRequestAwareAuthenticationSuccessHandler}是Spring Security預設的成功處理器
 * @author CaiRui
 * @date 2018-12-6 12:39
 */
@Component("tigerAuthenticationSuccessHandler")
@Slf4j
public class TigerAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
       log.info("登入成功");
       if(LoginTypeEnum.JSON.equals(securityProperties.getBrowser().getLoginType())){
           //返回json處理 預設也是json處理
           response.setContentType("application/json;charset=UTF-8");
           log.info("認證資訊:"+JSON.toJSONString(authentication));
           response.getWriter().write(JSON.toJSONString(authentication));
       } else {
           // 如果使用者定義的是跳轉,那麼就使用父類方法進行跳轉
           super.onAuthenticationSuccess(request, response, authentication);
       }

    }
}

TigerAuthenticationFailureHandler

package com.rui.tiger.auth.core.authentication;

import com.alibaba.fastjson.JSON;
import com.rui.tiger.auth.core.model.enums.LoginTypeEnum;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 認證失敗處理器
 * @author CaiRui
 * @date 2018-12-6 12:40
 */
@Component("tigerAuthenticationFailureHandler")
@Slf4j
public class TigerAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        log.info("登入失敗");
        if (LoginTypeEnum.JSON.equals(securityProperties.getBrowser().getLoginType())) {
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(JSON.toJSONString(exception));
        } else {
            // 如果使用者配置為跳轉,則跳到Spring Boot預設的錯誤頁面
            super.onAuthenticationFailure(request, response, exception);
        }

    }
}

ok 下面我們來測試下看我們的流程是否可以?

如果我們直接訪問 localhost:8070/user/hello 

這是因為我們預設配置了json,輸入我們的的登入表單地址localhost:8070/tiger-login.html,並輸入正確的賬戶密碼登入

可以看到已經返回認證成功的json字串,失敗處理器也會返回失敗的資訊這裡就不測試了。

到現在整個登入基本流程算是跑通了,下一章我們來簡單分析下spring-security的認證原始碼。

 

‘’

TigerAuthenticationFailureHandlerTigerAuthenticationFailureHandler