1. 程式人生 > >springboot整合spring-security實現登入控制的過程及其要點

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:費了一天功夫呢,掛個原創不過分吧。。。