看這篇文章的我默認都認為是有security基本基礎的宪拥,因為封裝的緣故,沒有基礎的話很容易被繞暈窃蹋。但是只要認真看卡啰,我相信沒什么大問題,如果僅僅是會用警没,該教程讓你更熟悉底層實現(xiàn)匈辱,更優(yōu)雅的寫代碼。
第一章順風車:SpringBoot 整合 Security(一)實現(xiàn)用戶認證并判斷返回json還是view
第二章順風車:SpringBoot 整合 Security(二)實現(xiàn)驗證碼登錄
本教程大概目錄:
- 實現(xiàn)用戶認證
- 實現(xiàn)json請求返回json杀迹,網(wǎng)頁請求返回網(wǎng)頁亡脸。
1. 添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2. 封裝以及實現(xiàn)
因為封裝的比較多,實現(xiàn)比較復雜,我想先把項目結構貼出來浅碾,然后我說明每個類之間的關系大州,然后把類一個一個再貼出來,這樣大家更容易理解一些(我每個類的注釋很全)垂谢。
2.1 結構
他們都在我的 com.fantJ
包下厦画。總共也就這么多類滥朱,但是關系比較復雜根暑,我先把每個類介紹一下,按照從上到下的順序徙邻。
- MyAuthenticationFailHandler.java 自定義登錄失敗處理器排嫌,如果登錄認證失敗,會跳到這個類上來處理缰犁。
- MyAuthenticationSuccessHandler 自定義登錄成功處理器躏率,如果登錄認證成功,會運行這個類民鼓。
- 我們可以看出薇芝,不論登錄成功還是失敗,都會路過我們自定義的處理器丰嘉,所以我們可以在這里重寫原來的方法夯到,實現(xiàn)根據(jù)請求頭類型返回相應的 json/view。
- SimpleResponse 返回類類型 POJO(可返回任意類型的結果)(封裝字符串饮亏、數(shù)字耍贾、集合等返回類型)
- BrowerSecurityController 登錄路徑請求類,
.loginPage("/authentication/require")
路幸,是個controller請求類荐开。判斷json/html 請求 返回不同的登錄認證結果 - BrowserSecurityConfig Security 配置類,它里面會說明登錄方式简肴、登錄頁面晃听、哪個url需要認證、注入登錄失敗/成功過濾器
- MyUserDetailsService 加載用戶數(shù)據(jù) , 返回UserDetail 實例 (里面包含用戶信息)砰识。
- BrowserProperties 讀取配置文件里的:
fantJ.security.browser.loginPage
等 屬性類 - LoginType 登錄類型 枚舉類
- SecurityProperties Security 屬性 類能扒,讀取配置文件里的:
fantJ.security
等屬性,里面包含了BrowserProperties 對象辫狼。
2.2 代碼
我稍微改變下順序初斑,盡量的有條理性,方便大家理清思路膨处。
- 我們先寫核心配置類见秤,BrowserSecurityConfig .
package com.fantJ.browser;
/**
* Security 配置類
* Created by Fant.J.
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 注入 Security 屬性類配置
*/
@Autowired
private SecurityProperties securityProperties;
/**
* 重寫PasswordEncoder 接口中的方法砂竖,實例化加密策略
* @return 返回 BCrypt 加密策略
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 注入 自定義的 登錄成功處理類
*/
@Autowired
private MyAuthenticationSuccessHandler mySuccessHandler;
@Autowired
private MyAuthenticationFailHandler myFailHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
String redirectUrl = securityProperties.getBrowser().getLoginPage();
//basic 登錄方式
// http.httpBasic()
//表單登錄 方式
http.formLogin()
.loginPage("/authentication/require")
//登錄需要經(jīng)過的url請求
.loginProcessingUrl("/authentication/form")
.successHandler(mySuccessHandler)
.failureHandler(myFailHandler)
.and()
//請求授權
.authorizeRequests()
//不需要權限認證的url
.antMatchers("/authentication/require",redirectUrl).permitAll()
//任何請求
.anyRequest()
//需要身份認證
.authenticated()
.and()
//關閉跨站請求防護
.csrf().disable();
}
}
我們可以看到,它里面聲明了登錄頁面url鹃答、哪個請求不需要認證就能訪問乎澄、調用自定義成功/失敗處理過濾器等,可是說是security的核心配置挣跋。
這里面有非常需要注意的一點三圆,就是必須要給loginPage 設置不需要權限認證,否則項目會陷入死鎖避咆,調用loginPage受到權限限制舟肉,然后返回loginPage,然后又受到限制...循環(huán)下去查库。
- 上面代碼首先就 找loginPage路媚,那我先把.loginPage("/authentication/require")相關的 視圖控制器 貼出來
package com.fantJ.browser;
/**
* 判斷json/html 請求 返回不同的結果
* @ 注解@ResponseStatus :響應狀態(tài)碼 UNAUTHORIZED(401, "Unauthorized")
* Created by Fant.J.
*/
/**
* 響應狀態(tài)碼 UNAUTHORIZED(401, "Unauthorized")
*/
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
@RestController
public class BrowerSecurityController {
/**
* 日志
*/
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 重定向 策略
*/
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
/**
* 把當前的請求緩存到 session 里去
*/
private RequestCache requestCache = new HttpSessionRequestCache();
/**
* 注入 Security 屬性類配置
*/
@Autowired
private SecurityProperties securityProperties;
/**
* 當需要身份認證時 跳轉到這里
*/
@RequestMapping("/authentication/require")
public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
//拿到請求對象
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest != null){
//獲取 跳轉url
String targetUrl = savedRequest.getRedirectUrl();
logger.info("引發(fā)跳轉的請求是:"+targetUrl);
//判斷 targetUrl 是不是 .html 結尾, 如果是:跳轉到登錄頁(返回view)
if (StringUtils.endsWithIgnoreCase(targetUrl,".html")){
String redirectUrl = securityProperties.getBrowser().getLoginPage();
redirectStrategy.sendRedirect(request,response,redirectUrl);
}
}
//如果不是,返回一個json 字符串
return new SimpleResponse("訪問的服務需要身份認證樊销,請引導用戶到登錄頁");
}
}
其中最主要的邏輯是判斷request請求對象中的getRedirectUrl() 的結果是不是.html 結尾整慎,如果是,則調用sendRedirect(request,response,redirectUrl)方法重定向到redirectUrl頁面围苫,其中redirectUrl是我們自定義的登錄頁面裤园。
如果不是.html 結尾,那就是json請求剂府,我們返回json 字符串提示信息拧揽。SimpleResponse 其實就是一個Object對象,然后實現(xiàn)了它的getter setter方法腺占,為的是結構化封裝淤袜,返回的是對象,而不僅僅是個字符串(返回對象的話衰伯,響應格式是:content:訪問的服務需要身份認證铡羡,請引導用戶到登錄頁 。返回字符串就只是"訪問的服務需要身份認證意鲸,請引導用戶到登錄頁"烦周,你讓接json數(shù)據(jù)的工作者怎么去接這段字符)。
- 我就先貼下SimpleResponse 代碼临扮,很簡單论矾,掃一眼就行
package com.fantJ.browser.support;
/**
* 返回類 工具 (可返回任意類型的結果)
* Created by Fant.J.
*/
public class SimpleResponse {
/**
* 返回 內容 (json格式)
*/
private Object content;
public SimpleResponse(Object content) {
this.content = content;
}
...getter and setter...
}
- 我們在代碼2中的controller中,也調用了 我們自己寫的一個類 SecurityProperties杆勇,它是用來獲取 application.properties 中的配置屬性的。
package com.fantJ.core.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Security 屬性 類
* Created by Fant.J.
*/
//獲取配置屬性前綴
@ConfigurationProperties(prefix = "fantJ.security")
public class SecurityProperties {
/**
* 瀏覽器 屬性類
*/
private BrowserProperties browser = new BrowserProperties();
public BrowserProperties getBrowser() {
return browser;
}
public void setBrowser(BrowserProperties browser) {
this.browser = browser;
}
}
可以看到饱亿。它里面包含了一個對象BrowserProperties 蚜退,它也是讀取配置屬性的一個類
package com.fantJ.core.properties;
/**
* browser(瀏覽器)配置文件里的: fantJ.security.browser.loginPage 屬性類
* Created by Fant.J.
*/
public class BrowserProperties {
/**
* loginPage 默認值 是login.html
* 如果 application.properties 里有對 fantJ.security.browser.loginPage 的聲明闰靴,則獲取該值
*/
private String loginPage = "/browser-login.html";
/**
* 默認 返回 json 類型
*/
private LoginType loginType = LoginType.JSON;
public String getLoginPage() {
return loginPage;
}
public void setLoginPage(String loginPage) {
this.loginPage = loginPage;
}
public LoginType getLoginType() {
return loginType;
}
public void setLoginType(LoginType loginType) {
this.loginType = loginType;
}
}
然后我把配置文件貼出來。
#登錄頁 配置
fantJ.security.browser.loginPage = /demo-signIn.html
# 返回 類型設置(view 還是 json)
fantJ.security.browser.loginType = REDIRECT
總的來說钻注,SecurityProperties可以獲取到前綴為fantJ.security
的所有屬性蚂且,BrowserProperties可以獲取到fantJ.security.browser
下的所有屬性,所以BrowserProperties中會有對應的兩個字段loginPage 幅恋、loginType 杏死。
其中l(wèi)oginType 也是一個封裝枚舉類,特簡單的枚舉
package com.fantJ.core.properties;
/**
* 登錄類型 枚舉類
* Created by Fant.J.
*/
public enum LoginType {
REDIRECT,
JSON
}
- 配置都有了捆交,那接下來應該要寫如何去認證用戶淑翼。MyUserDetailsService里面可以用來獲取數(shù)據(jù)庫中的密碼然后打包返回用戶信息給security做用戶校驗使用,后者校驗如果與登錄的密碼match品追,如果成功玄括,返回UserDetail對象(用戶信息對象),進入自定義登錄成功后處理類MyAuthenticationSuccessHandler。如果失敗肉瓦,直接進入登錄失敗處理類MyAuthenticationFailHandler遭京。
MyUserDetailsService .java
package com.fantJ.browser;
/**
* UserDetail 類
* Created by Fant.J.
*/
@Component
public class MyUserDetailsService implements UserDetailsService {
// @Autowired
// private //在這里注入mapper,再想ia面根據(jù)用戶名做信息查找
/**
* 重寫PasswordEncoder 接口中的方法泞莉,實例化加密策略
* @return 返回 BCrypt 加密策略
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
private PasswordEncoder passwordEncoder;
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 加載用戶數(shù)據(jù) , 返回UserDetail 實例
* @param username 用戶登錄username
* @return 返回User實體類 做用戶校驗
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("登錄用戶名:"+username);
String password = passwordEncoder.encode("123456");
//User三個參數(shù) (用戶名+密碼+權限)
//根據(jù)查找到的用戶信息判斷用戶是否被凍結
logger.info("數(shù)據(jù)庫密碼:"+password);
return new User(username,password,
true,true,true,true,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
上面有段代碼
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
PasswordEncoder 是個接口哪雕,該接口下有兩個方法,一個是encoder 一個是matches鲫趁,前者用于加密斯嚎,后者用于匹配校驗,我們這里使用的是BCrypt加密算法來實現(xiàn)加密和匹配饮寞,所以在這里實現(xiàn)該接口的encoder方法孝扛,進行加密。
其次我想說的是重寫的loadUserByUsername方法幽崩,該方法將用戶登錄使用的username傳進來苦始,然后我們把該用戶的密碼從數(shù)據(jù)庫取出來,一同打包成User對象慌申,返回給security框架做下一步校驗陌选。其中User的幾個參數(shù)介紹見下面源碼:
public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
...
}
依次是:用戶名+密碼+可用?+沒過期蹄溉?+授權過期咨油?+不被鎖?+用戶權限(我在這里是手動加了個權限和密碼柒爵,自己根據(jù)業(yè)務修改下)
然后帶大家看下security內部是怎樣校驗用戶身份的役电。
省略了 很多個步驟,具體的大家可以自己打斷點 調試下棉胀。
- 最后我把成功/失敗處理器代碼貼出來
MyAuthenticationSuccessHandler .java
package com.fantJ.browser.authentication;
/**
* 自定義登錄成功處理類
* Created by Fant.J.
*/
@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
/**
* 日志
*/
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* json 轉換工具類
*/
private ObjectMapper objectMapper;
@Autowired
private SecurityProperties securityProperties;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
logger.info("登錄成功");
//判斷是json 格式返回 還是 view 格式返回
if (LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){
//將 authention 信息打包成json格式返回
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}else {
//返回view
super.onAuthenticationSuccess(request,response,authentication);
}
}
}
MyAuthenticationFailHandler .java
package com.fantJ.browser.authentication;
/**
* 自定義登錄失敗處理器
* Created by Fant.J.
*/
@Component
public class MyAuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler {
/**
* 日志
*/
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* json 轉換工具類
*/
private ObjectMapper objectMapper;
@Autowired
private SecurityProperties securityProperties;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
logger.info("登錄失敗");
//如果是json 格式
if (LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){
//設置狀態(tài)碼
response.setStatus(500);
//將 登錄失敗 信息打包成json格式返回
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(e));
}else{
//如果不是json格式法瑟,返回view
super.onAuthenticationFailure(request,response,e);
}
}
}
他倆再哪里被調用了呢冀膝,再BrowserSecurityConfig類里,也就是security啟動核心配置類中霎挟,注入并
寫起來好麻煩窝剖,希望大家能看懂,有什么疑問的可以在下方留言酥夭。謝謝大家赐纱!
介紹下我的所有文集:
流行框架
SpringCloud
springboot
nginx
redis
底層實現(xiàn)原理:
Java NIO教程
Java reflection 反射詳解
Java并發(fā)學習筆錄
Java Servlet教程
jdbc組件詳解
Java NIO教程
Java語言/版本 研究