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