Spring Security + JWT token 做權限認證

前言

我的項目是用SpringBoot 搭建的一個App-Server,用來響應移動端的訪問請求,設計的方式是前後端分離的 。本來對權限的做法是在請求裏面加上token 字段,然後服務器端再對token做解析,得到userid,再根據userid 查找數據庫,來判斷當前用戶是否有權限訪問這個接口。token 是用的JWT;這樣做除了每個接口都要寫解析token 和 權限的判斷代碼外,感覺也沒有其他問題。

哪位有經驗的兄弟能解答一下這樣做有什麼不妥的地方嗎?不甚感激。測試接口和服務端代碼如下:

	@PostMapping("/test")
	@ResponseBody
	public BaseResponse test(@RequestBody BaseQueryDataReq req) {
		
		BaseResponse rsp = new BaseResponse();
		
		if(req.getToken() == null || req.getToken().isEmpty())
			return  ResponseFactory.getErrorResponse(ErrorEnum.ERROR_TOKEN_ERROR);
		else if(JwtHelper.isTokenExpiration(req.getToken()))
			return  ResponseFactory.getErrorResponse(ErrorEnum.ERROR_TOKEN_EXPIRE);
		
		String userid = JwtHelper.getUserIdFromToken(req.getToken());
		if(userid == null || userid.isEmpty() || !req.getUserid().equals(userid))
			return ResponseFactory.getErrorResponse(ErrorEnum.ERROR_TOKEN_ERROR);
		...
        ...
    }

這個方法是參考各大開發者平臺的接口定義來的;他們的接口中,大部分都需要在json裏面加上一個token字段。但看了Spring Security的一些文章後,覺得不用這個就感覺不正宗一樣,所以我也嘗試着研究Spring Security。

 

 

基礎概念

RESTful API認證方式

一般來講,對於RESTful API都會有認證(Authentication)和授權(Authorization)過程,保證API的安全性。

Authentication vs. Authorization

Authentication指的是確定這個用戶的身份(用戶賬號),Authorization是確定該用戶擁有什麼操作權限,(用戶角色role)。

認證方式一般有三種

  • Basic Authentication

這種方式是直接將用戶名和密碼放到Header中,使用Authorization: Basic Zm9vOmJhcg==,使用最簡單但是最不安全。

  • TOKEN認證

這種方式也是再HTTP頭中,使用Authorization: Bearer <token>,使用最廣泛的TOKEN是JWT,通過簽名過的TOKEN。

  • OAuth2.0

這種方式安全等級最高,但是也是最複雜的。如果不是大型API平臺或者需要給第三方APP使用的,沒必要整這麼複雜。

一般項目中的RESTful API使用JWT來做認證就足夠了。

 

spring security認證的實現方式:

實現方式大致可以分爲這幾種:

    1.配置文件實現,只需要在配置文件中指定攔截的url所需要權限、配置userDetailsService指定用戶名、密碼、對應權限,就可以實現。

    2.實現UserDetailsService,loadUserByUsername(String userName)方法,根據userName來實現自己的業務邏輯返回UserDetails的實現類,需要自定義User類實現UserDetails,比較重要的方法是getAuthorities(),用來返回該用戶所擁有的權限。

    3.通過自定義filter重寫spring security攔截器,實現動態過濾用戶權限。

    4.通過自定義filter重寫spring security攔截器,實現自定義參數來檢驗用戶,並且過濾權限。

我要講的就是第二種方式,用自定義的User類來實現UserDetails,UserDetails的源碼如下, 從源碼可以看出,我們需要重點實現的是獲取用戶權限,用戶名,用戶密碼這三個接口;簡單實現見JwtUserDetails;

public interface UserDetails extends Serializable {
	// ~ Methods
	// ========================================================================================================

	/**
	 * Returns the authorities granted to the user. Cannot return <code>null</code>.
	 *
	 * @return the authorities, sorted by natural key (never <code>null</code>)
	 */
	Collection<? extends GrantedAuthority> getAuthorities();

	/**
	 * Returns the password used to authenticate the user.
	 *
	 * @return the password
	 */
	String getPassword();

	/**
	 * Returns the username used to authenticate the user. Cannot return <code>null</code>.
	 *
	 * @return the username (never <code>null</code>)
	 */
	String getUsername();

	/**
	 * Indicates whether the user's account has expired. An expired account cannot be
	 * authenticated.
	 *
	 * @return <code>true</code> if the user's account is valid (ie non-expired),
	 * <code>false</code> if no longer valid (ie expired)
	 */
	boolean isAccountNonExpired();

	/**
	 * Indicates whether the user is locked or unlocked. A locked user cannot be
	 * authenticated.
	 *
	 * @return <code>true</code> if the user is not locked, <code>false</code> otherwise
	 */
	boolean isAccountNonLocked();

	/**
	 * Indicates whether the user's credentials (password) has expired. Expired
	 * credentials prevent authentication.
	 *
	 * @return <code>true</code> if the user's credentials are valid (ie non-expired),
	 * <code>false</code> if no longer valid (ie expired)
	 */
	boolean isCredentialsNonExpired();

	/**
	 * Indicates whether the user is enabled or disabled. A disabled user cannot be
	 * authenticated.
	 *
	 * @return <code>true</code> if the user is enabled, <code>false</code> otherwise
	 */
	boolean isEnabled();
}
public class JwtUserDetails implements UserDetails {

	private String userName;
	private String password;
	
	private Collection<? extends GrantedAuthority> authorities;
	
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return authorities;
	}

	@Override
	public String getPassword() {
		return password;
	}

	@Override
	public String getUsername() {

		return userName;
	}

	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {

		return true;
	}

	@Override
	public boolean isEnabled() {
		return true;
	}

}

 爲什麼要搞這麼一個UserDetails 呢,用來幹嘛的呢?這個沒有太明白,估計得看源碼才能徹底搞明白裏面的流程。

    Spring Security中進行身份驗證的是AuthenticationManager接口,ProviderManager是它的一個默認實現,但它並不用來處理身份認證,而是委託給配置好的AuthenticationProvider,每個AuthenticationProvider會輪流檢查身份認證。檢查後或者返回Authentication對象或者拋出異常。

    驗證身份就是加載相應的UserDetails,看看是否和用戶輸入的賬號、密碼、權限等信息匹配。此步驟由實現AuthenticationProvider的DaoAuthenticationProvider(它利用UserDetailsService驗證用戶名、密碼和授權)處理。包含 GrantedAuthority 的 UserDetails對象在構建 Authentication對象時填入數據。

 

 

最終我需要實現的就是:

1,在login 接口中 返回JWT 的token,其中攜帶username 信息;

2,在Spring Security框架中,自定義一個filter ,在 UserNamePasswordFilter 之前進行驗證,驗證通過後,寫入SpringSecurityContext;

3,除獲取token 之外的接口(註冊,登錄)其他接口需要攜帶token 訪問;

配置文件如下:


@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	JwtUserDetailService jwtUserDetailService;

	@Autowired
	public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
		authenticationManagerBuilder
				// 設置UserDetailsService
				.userDetailsService(jwtUserDetailService)
				// 使用BCrypt進行密碼的hash
				.passwordEncoder(passwordEncoder());
	}

	// 裝載BCrypt密碼編碼器
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(jwtUserDetailService);
	}


	@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
	    return super.authenticationManagerBean();
	}
	
	@Override
	protected void configure(HttpSecurity httpSecurity) throws Exception {

		httpSecurity
		.authorizeRequests()
        .antMatchers("/auth").authenticated()       // 需攜帶有效 token
        .antMatchers("/admin").hasAuthority("admin")   // 需擁有 admin 這個權限
        .antMatchers("/ADMIN").hasRole("ADMIN")     // 需擁有 ADMIN 這個身份
        .antMatchers("/register").permitAll()
        .antMatchers("/login").permitAll()
        .anyRequest().authenticated()       // 允許所有請求通過
        .and()
        .csrf()
        .disable()                      // 禁用 Spring Security 自帶的跨域處理
        .sessionManagement()                        // 定製我們自己的 session 策略
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 調整爲讓 Spring Security 不創建和使用 session

		httpSecurity.addFilterBefore(authenticationTokenFilterBean(),JwtTokenFilter.class);
	}


	@Bean
	public JwtTokenFilter authenticationTokenFilterBean() throws Exception {
		JwtTokenFilter authenticationTokenFilter = new JwtTokenFilter(authenticationManagerBean());
		authenticationTokenFilter.setAuthenticationManager(authenticationManagerBean());
		return authenticationTokenFilter;
	}
	

filter 如下:

@Component
public class JwtTokenFilter extends UsernamePasswordAuthenticationFilter {

	/**
	 * json web token 在請求頭的名字
	 */
	@Value("${token.header}")
	private String tokenHeader;

	
	/**
	 * 輔助操作 token 的工具類
	 */
	@Autowired
	private JwtTokenUtils tokenUtils;

	@Autowired
	private JwtUserDetailService userDetailsService;

	@Autowired
	private JwtTokenUtils jwtTokenUtil;

	public JwtTokenFilter(AuthenticationManager authenticationManager) {
		setAuthenticationManager(authenticationManager);
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {

		// 將 ServletRequest 轉換爲 HttpServletRequest 才能拿到請求頭中的 token
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        // 嘗試獲取請求頭的 token
        String authToken = httpRequest.getHeader(this.tokenHeader);
        System.out.println("getHeader(\"Authorization\")" + httpRequest.getHeader("Authorization"));
        // 嘗試拿 token 中的 username
        // 若是沒有 token 或者拿 username 時出現異常,那麼 username 爲 null
        String username = this.tokenUtils.getUsernameFromToken(authToken);
        
        // 如果上面解析 token 成功並且拿到了 username 並且本次會話的權限還未被寫入
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            if (this.tokenUtils.validateToken(authToken, userDetails)) {
                // 生成通過認證
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
                // 將權限寫入本次會話
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }

        }


		chain.doFilter(request, response);
	}

API 接口如下: login ; register 接口不需要token可以訪問; query_test 接口需要在header 裏攜帶token

@RestController
public class MainControler {
	@Autowired
	UserRepository  userRepository;

	@Autowired
	JwtTokenUtils tokenUtils;
	
	@PostMapping("/login")
	@ResponseBody
	public BaseReq login(@RequestBody AppUser req)
	{
		
		AppUser u = userRepository.findByUsername(req.getUsername());
		BaseReq rsp = new BaseReq();
		String token = tokenUtils.generateToken(req.getUsername());
		rsp.setToken(token);
		return rsp;
	}
	
	@PostMapping("/register")
	@ResponseBody
	public AppUser register(@RequestBody AppUser req)
	{
		AppUser u = userRepository.findByUsername(req.getUsername());
		if(u == null)
			u = userRepository.save(req);
		else
			u = userRepository.save(u);
		
		String token = tokenUtils.generateToken(req.getUsername());
		System.out.println("token : "+ token);
		return u;
	}
	
	@PostMapping("/query_test")
	@ResponseBody
	public AppUser query(@RequestBody AppUser req)
	{
		AppUser u = userRepository.findByUsername(req.getUsername());
		return u;
	}

 

不帶token 訪問時,提示403 ,無權限;攜帶token 後,能正常訪問。

https://blog.csdn.net/my_learning_road/article/details/79833802

https://www.xncoding.com/2017/07/09/spring/sb-jwt.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章