spring security 学习一
spring security 學(xué)習(xí)一
1、配置基本的springboot web項目,加入security5依賴,啟動項目
瀏覽器訪問,即可出現(xiàn)一個默認(rèn)的登錄頁面
2、什么都沒有配置 登錄頁面哪里來的
一般不知從何入手,就看官方文檔里是如何做的,官方的文檔和api 是最好最完整的介紹和參考,點(diǎn)擊鏈接查看官方文檔地址
(https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#jc-oauth2login),或者通過Google 搜索 spring security,
在結(jié)果中點(diǎn)擊Spring Security Reference,點(diǎn)擊進(jìn)入頁面,然后就可以看到關(guān)于Spring Security的文檔;
通過查看文檔發(fā)現(xiàn),WebSecurityConfigurerAdapter 提供的默認(rèn)的配置,config(HttpSecurty http)中的formLogin(),這個方法內(nèi)容如下:
public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {return (FormLoginConfigurer)this.getOrApply(new FormLoginConfigurer());}查看formLogin()源碼,跳轉(zhuǎn)到HttpSecurity類中,這個方法返回一個FormLoginConfigurer<HttpSercurity>類型的數(shù)據(jù)。再繼續(xù)來看看這
個FormLoginConfigurer,在FormLoginConfigurer中有個initDefaultLoginFilter()方法:
private void initDefaultLoginFilter(H http) {DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = (DefaultLoginPageGeneratingFilter)http.getSharedObject(DefaultLoginPageGeneratingFilter.class);if (loginPageGeneratingFilter != null && !this.isCustomLoginPage()) {loginPageGeneratingFilter.setFormLoginEnabled(true);loginPageGeneratingFilter.setUsernameParameter(this.getUsernameParameter());loginPageGeneratingFilter.setPasswordParameter(this.getPasswordParameter());loginPageGeneratingFilter.setLoginPageUrl(this.getLoginPage());loginPageGeneratingFilter.setFailureUrl(this.getFailureUrl());loginPageGeneratingFilter.setAuthenticationUrl(this.getLoginProcessingUrl());}}這個方法,初始化一個默認(rèn)登錄頁的過濾器,可以看到第一句代碼,默認(rèn)的過濾器是DefaultLoginPageGeneratingFilter,下面是設(shè)置一些必要的參數(shù),進(jìn)入到這個過濾器中:
在描述中可以看到,如果沒有配置login頁,這個過濾器會被創(chuàng)建,過濾器創(chuàng)建后再瀏覽器訪問的時候回指定doFilter()方法:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;boolean loginError = isErrorPage(request);boolean logoutSuccess = isLogoutSuccess(request);if (isLoginUrlRequest(request) || loginError || logoutSuccess) {String loginPageHtml = generateLoginPageHtml(request, loginError,logoutSuccess);response.setContentType("text/html;charset=UTF-8");response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);response.getWriter().write(loginPageHtml);return;}chain.doFilter(request, response);}登錄頁面的配置是通過generateLoginPageHtml()方法創(chuàng)建的,再來看看這個方法內(nèi)容:
private String generateLoginPageHtml(HttpServletRequest request, boolean loginError,boolean logoutSuccess) {String errorMsg = "Invalid credentials";if (loginError) {HttpSession session = request.getSession(false);if (session != null) {AuthenticationException ex = (AuthenticationException) session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);errorMsg = ex != null ? ex.getMessage() : "Invalid credentials";}}StringBuilder sb = new StringBuilder();sb.append("<!DOCTYPE html>\n"+ "<html lang=\"en\">\n"+ " <head>\n"+ " <meta charset=\"utf-8\">\n"+ " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n"+ " <meta name=\"description\" content=\"\">\n"+ " <meta name=\"author\" content=\"\">\n"+ " <title>Please sign in</title>\n"+ " <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n"+ " <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n"+ " </head>\n"+ " <body>\n"+ " <div class=\"container\">\n");String contextPath = request.getContextPath();if (this.formLoginEnabled) {sb.append(" <form class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.authenticationUrl + "\">\n"+ " <h2 class=\"form-signin-heading\">Please sign in</h2>\n"+ createError(loginError, errorMsg)+ createLogoutSuccess(logoutSuccess)+ " <p>\n"+ " <label for=\"username\" class=\"sr-only\">Username</label>\n"+ " <input type=\"text\" id=\"username\" name=\"" + this.usernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n"+ " </p>\n"+ " <p>\n"+ " <label for=\"password\" class=\"sr-only\">Password</label>\n"+ " <input type=\"password\" id=\"password\" name=\"" + this.passwordParameter + "\" class=\"form-control\" placeholder=\"Password\" required>\n"+ " </p>\n"+ createRememberMe(this.rememberMeParameter)+ renderHiddenInputs(request)+ " <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n"+ " </form>\n");}if (openIdEnabled) {sb.append(" <form name=\"oidf\" class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.openIDauthenticationUrl + "\">\n"+ " <h2 class=\"form-signin-heading\">Login with OpenID Identity</h2>\n"+ createError(loginError, errorMsg)+ createLogoutSuccess(logoutSuccess)+ " <p>\n"+ " <label for=\"username\" class=\"sr-only\">Identity</label>\n"+ " <input type=\"text\" id=\"username\" name=\"" + this.openIDusernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n"+ " </p>\n"+ createRememberMe(this.openIDrememberMeParameter)+ renderHiddenInputs(request)+ " <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n"+ " </form>\n");}if (oauth2LoginEnabled) {sb.append("<h2 class=\"form-signin-heading\">Login with OAuth 2.0</h2>");sb.append(createError(loginError, errorMsg));sb.append(createLogoutSuccess(logoutSuccess));sb.append("<table class=\"table table-striped\">\n");for (Map.Entry<String, String> clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) {sb.append(" <tr><td>");String url = clientAuthenticationUrlToClientName.getKey();sb.append("<a href=\"").append(contextPath).append(url).append("\">");String clientName = HtmlUtils.htmlEscape(clientAuthenticationUrlToClientName.getValue());sb.append(clientName);sb.append("</a>");sb.append("</td></tr>\n");}sb.append("</table>\n");}sb.append("</div>\n");sb.append("</body></html>");return sb.toString();}?
3、去掉默認(rèn)的登錄頁,修改application.yml,添加一下內(nèi)容(在security5中不在支持以下配置,而是提供一個自定義的WebSecurityConfigurer文件)
The security auto-configuration is no longer customizable. Provide your own WebSecurityConfigurer bean instead. security:basic:enabled: false?
4、自定義WebSecurityConfigurer
以下配置是創(chuàng)建一個最簡單的基于form表單認(rèn)證的security
formLogin():指定認(rèn)證為form表單
authorizeRequests():授權(quán)
anyRequest():任何請求
authenticated():都需要認(rèn)證
@Configuration public class CusWebSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin()//指定是表單登錄.and().authorizeRequests()//授權(quán).anyRequest()//任何請求.authenticated();//都需要身份認(rèn)證} }?
5、基本流程
過濾器鏈有以下:
①UsernamePasswordAuthenticationFilter
在過濾器容器中判斷請求中是否有用戶名和密碼,如果有用戶名和密碼就會使用UsernamePasswordAuthenticationFilter這個過濾器,如果沒有就會走下一個過濾器
②BasicAuthenticationFilter
在這個過濾器中回嘗試獲取請求頭中是否有basic開頭的Authentication信息,如果有
就會嘗試解碼,處理完成之后會走下一個filter
③ExceptionTranslationFilter
?? 這個過濾器的作用是用來捕獲下邊這個FilterSecurityInterceptor拋出的異常
④FilterSecurityInterceptor
這個攔截器是過濾器鏈中的最后一環(huán),在這個里邊會判斷當(dāng)前請求能否訪問controller,
能否訪問是根據(jù)securityconfig配置來判斷的
即:
?
?6、源碼學(xué)習(xí)
FilterSecurityInterceptor關(guān)鍵源碼
在invoke方法中有一個super.beforeInvocation方法,如上圖,綠色的過濾器鏈都是在這個方法中進(jìn)行處理的
public void invoke(FilterInvocation fi) throws IOException, ServletException {if ((fi.getRequest() != null)&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)&& observeOncePerRequest) {// filter already applied to this request and user wants us to observe// once-per-request handling, so don't re-do security checkingfi.getChain().doFilter(fi.getRequest(), fi.getResponse());}else {// first time this request being called, so perform security checkingif (fi.getRequest() != null && observeOncePerRequest) {fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);}InterceptorStatusToken token = super.beforeInvocation(fi);try {fi.getChain().doFilter(fi.getRequest(), fi.getResponse());}finally {super.finallyInvocation(token);}super.afterInvocation(token, null);}}當(dāng)訪問api:localhost:9999/h? 時,請求回走到FIlterSecurityInterceptor中的beforeInvocation處
因為自己的securityConfig的是所有的請求都需要進(jìn)行認(rèn)證,因此在執(zhí)行befoeInvocation的時候會拋出一個異常,也就是進(jìn)入了ExceptionTranlationFilter中(因為沒有經(jīng)過認(rèn)證,不能訪問api)
在ExceptionTranlationFilter對異常進(jìn)行處理,也就是把請求重定向到login頁面上。
?
在登錄頁面填寫登錄名和密碼(user/4ed8dccc-9425-4f92-8b12-0bac0d88793b,密碼后臺日志會2自動輸出),填寫完畢后點(diǎn)擊登錄,又因為使用的是form表單登錄,所以會進(jìn)入到UserNamePasswordAuthenticationFilter中,
在UsernamepasswordAuthenticationFilter中執(zhí)行完畢后,回再次進(jìn)入到FilterSecurityInteceptor中的beforeInvocation處,此時執(zhí)行到該處是不會報錯,回向下繼續(xù)進(jìn)行。
?
調(diào)用doFilter,也就是進(jìn)入了自己寫的api接口中(controller中)
?
整個的流程:FilterSecurityInterceptor攔截請求,沒有認(rèn)證,重定向到默認(rèn)的form認(rèn)證頁面(login),在登錄頁面輸入用戶名密碼,點(diǎn)擊登錄后,會進(jìn)入到UsernamePasswordAuthenticationFilter中(因為使用的form表單認(rèn)證,如果使用其他認(rèn)證的話,會進(jìn)入到其他Filter中),在UsernamePasswordAuthenticationFilter中執(zhí)行完畢后,回再次進(jìn)入FIlterSecurityInterceptor中,執(zhí)行沒問題后,最終到controller層中的api處
?
?自定義認(rèn)證邏輯
一、處理用戶信息獲取邏輯
在security中用戶信息的獲取中,提供了一個接口UserDetailService,該接口中只有一個方法loadUserByUsername,返回參數(shù)是一個UserDetail
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;在獲取用戶信息是,只需要關(guān)注一點(diǎn)即,獲取UserDetail用戶信息,之后的認(rèn)證都是基于此對象的,
當(dāng)查不到一個username是,會拋出一個UsernameNotFoundException異常,可以進(jìn)行異常統(tǒng)一攔截
1、新建MyUserDetailsService(數(shù)據(jù)都是寫死的)
(ps:security5好像不能只寫一個userdetailservice就行運(yùn)行,也得配一個PasswordEncoder,即在security配置文件中添加)
@Component @Slf4j public class MyUserDetailsService implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {//根據(jù)username查找用戶信息,在這,先手動寫一個user信息log.info("查找用戶信息{}", s);//密碼在security5中好像得加密? 不加密的話會爆粗(不確定)
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String password = encoder.encode("password");
添加w完畢后,訪問localhost:9999/h api,先回跳轉(zhuǎn)到默認(rèn)登錄頁面:
隨便輸入username和password:會出現(xiàn)bad credentials信息
如果密碼輸入password(后臺user自定義加密后的password):會登錄成功,并會執(zhí)行controller
?
二、處理用戶校驗邏輯
1、 用戶的校驗邏輯主要就是比較密碼是否匹配,這一塊有security自動匹配(即將user信息放到Userdetail解耦的實現(xiàn)類中即可)
2、賬號是否過期、是否被鎖定、是否可用,這幾個校驗都可以重新以下方法(如果沒有對應(yīng)的邏輯,永遠(yuǎn)返回true即可)
boolean isAccountNonExpired();//賬號沒有過期 如果不需要的話,改為true,沒有過期boolean isAccountNonLocked(); //賬號沒有鎖定鎖定boolean isCredentialsNonExpired();//密碼是否過期了boolean isEnabled();//這個可以配到庫中3、自己測試
修改loadUserByUsername方法的返回參數(shù)
①accountNonLock設(shè)為false
@Component @Slf4j public class MyUserDetailsService implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {//根據(jù)username查找用戶信息,在這,先手動寫一個user信息log.info("查找用戶信息{}", s);BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();String password = encoder.encode("password");//user前兩個參數(shù)是進(jìn)行認(rèn)證的,第三個參數(shù)是當(dāng)前用戶所擁有的權(quán)限,security回根據(jù)授權(quán)代碼進(jìn)行驗證return new User(s, password, true, true, true, false,AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); // AuthorityUtils.commaSeparatedStringToAuthorityList 這個方法是將一個字符一“,” 分割開} }修改問之后,重啟測試用的項目后,方法api,輸入用戶密碼后,提示已被鎖定(即accountNonLock屬性被設(shè)為false)
?
三、處理密碼加密解密
以下兩種方式,都可以使用到加密
@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(myUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());}或:
@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}?
posted @ 2019-04-22 21:37 巡山小妖N 閱讀(...) 評論(...) 編輯 收藏
總結(jié)
以上是生活随笔為你收集整理的spring security 学习一的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: spring-data-jpa 二、多对
- 下一篇: 拦截请求并记录相应信息-springbo