springboot整合spring-security實現登入控制的過程及其要點
前言
首先,整合spring-security的目的
1,實現登入控制;
2,防止同一賬號的同時多處登入。
3,實現臺介面的訪問許可權控制。
實現方式不止一種,選擇spring-security是因為它夠簡潔。
實現
闡述兩種實現方式--不用框架、採用spring-security
不用框架
問題1
不用框架的話,實現前言中的問題1(下文簡稱:問題1)可以在每次請求時,先獲取下session,然後判斷下該session是否已經登入。
如何判斷是否登入?可以往session中插入內容嘛,其實就是將該session與具體的某個帳號關聯起來。
具體實現不贅述了,因為這種方式實在太普遍了,百度下滿地都是,它不是我要記敘的重點。
PS:每次請求都去判斷下是不是很繁瑣,這時候你該考慮攔截器,前置處理所有請求
問題2
解決問題2也簡單,統一維護所有session,新加入的session和老的比對下。如果對映的帳號是同一個就執行控制策略,比如:踢掉舊的,保留新的。
問題3
一樣,原理是控制每次請求時,該session對應帳號的許可權,攔截器可以有效統一處理。
採用spring-security
每次遇到普遍性的問題,就該去想想這類是否有統一的解決方法。
針對上述3個問題,找到了spring-security,而且它不僅僅侷限於此,只是我暫時只需要用到它這3個功能。spring全家桶越用越舒服,我是真的佩服這些做開源免費軟體的。
spring-security解決上述3個問題,就是一份配置
import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.util.DigestUtils; @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UserService userService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() { @Override public String encode(CharSequence charSequence) { return charSequence.toString(); } /** * @param charSequence 明文 * @param s 密文 * @return */ @Override public boolean matches(CharSequence charSequence, String s) { return s.equals(charSequence.toString()); } }); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // .antMatchers("/test/security/**").hasRole("超級管理員") .anyRequest().authenticated()//其他的路徑都是登入後即可訪問 .and().formLogin().loginPage("/test/security/login").successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); out.write("{\"status\":\"ok\",\"msg\":\"登入成功\"}"); out.flush(); out.close(); } }).failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); out.write("{\"status\":\"error\",\"msg\":\"登入失敗\"}"); out.flush(); out.close(); } }).loginProcessingUrl("/test/security/loginP")//登入地址 .usernameParameter("username").passwordParameter("password").permitAll() .and().logout().permitAll().and().csrf().disable() .cors();//新增cors支援跨域 http.sessionManagement().maximumSessions(1).expiredUrl("/test/security/login"); //防止多處登入 } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/error", "/swagger-ui.html", "/back/admin/getCountInfo", "/swagger-resources/**", "/v2/api-docs", "/api/**", "/pub/**");//pub:用於測試,swagger測試用? } } import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; @Service public class UserService implements UserDetailsService { @Autowired private UserRepo userRepo; @Override public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException { return userRepo.findFisrtByName(name); } } import java.util.ArrayList; import java.util.Collection; import java.util.List; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @Entity public class UserEnt implements UserDetails { @Id @GeneratedValue(strategy=GenerationType.AUTO) private Long id; private String name; private String passwd; private String role; public UserEnt() {} public UserEnt(String name, String passwd, String role) { this.name = name; this.passwd = passwd; this.role = role; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPasswd() { return passwd; } public void setPasswd(String passwd) { this.passwd = passwd; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority(role)); return authorities; } @Override public String getPassword() { return passwd; } @Override public String getUsername() { return name; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } @Override public String toString() { return this.name; } @Override public int hashCode() { return name.hashCode(); } @Override public boolean equals(Object obj) { return this.toString().equals(obj.toString()); } // @Override // public boolean equals(Object obj) { // UserEnt target = (UserEnt) obj; // return name.equals(target.getName()); // } }
程式碼不多貼了,能看清關鍵邏輯即可。其實很多博文讓人觀感難受的一大原因就是–上來一堆程式碼,全文無總結。當然,我也常這麼幹,因為部落格的首要作用是自我總結,先是自己的筆記本,之後才是贈人的玫瑰。但是這篇我還是想記敘的明白一些。
首先,我spring-security解決這3個問題的原理其實也就是對session的維護以及對各介面的許可權控制。所以,配置邏輯其實就是圍繞著這個來的。
歸納下配置邏輯:
1,spring-security需要建立session和使用者的關係。所以,userService實現了UserDetailsService這個介面,這個介面是用來獲取使用者資訊的。
2,登入是傳入的密碼通常是加密的,你可以在第一個configure中做相應處理。我 只是做測試所以未處理,具體可以參考下文的幾篇參考連結,這類問題百度不難解決。
3,配置登入地址,登入頁面,登入成功及失敗後反饋。
4,總有一些介面不想被攔截的,那麼就需要在最後一個configure中剔除掉。
解釋
這部分及接下去的要點其實才是最重要的,是對關鍵方法的說明。那些直接copy然後看著方法名和註釋就能明白的部分就不贅述了。
1,loginPage中的引數是登入頁。所謂登入頁,就是未登入時訪問在攔截範圍內的頁面會觸發302跳轉到此處。它不非得是一個頁面,可以是返回一串json內容。所以,很靈活是不是。
2,loginProcessingUrl是spring-security的登入地址。你想啊,你得讓spring-security來維護session,要麼是你把session甩給它,要麼是直接經過它登入,總得讓它能把session和使用者關聯起來。最簡單的就是後者–直接經過它登入,然後它會根據登入結果來控制各介面訪問許可權。
3,successHandler&failureHandler就是登入成功和失敗後的返回。
4,防止多處登入其實就是增加一句程式碼配置。此處的配置邏輯是–新的登入覆蓋舊的,可以按需配置為不允許新的再次登入,此處不展開了。
5,todo:介面許可權控制,暫時沒測試。
小結,其實spring-security就是把本來該我們自己做的事,替我們做了,而且做得更更好。
要點
這裡,還有幾個要點要說下:
1,loginProcessingUrl中定義的spring-security登入地址必須用post方式請求,get是無效的會被一併攔截。
2,要防止多處登入,首先就是使用者的比對,所以,實現UserDetails的那個類(UserEnt)的equal方法寫的時候規範點。
3,要開啟跨域的話,原先springboot的跨域配置要保留外,還得呼叫cors()方法。
參考
PS:費了一天功夫呢,掛個原創不過分吧。。。