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

猜你喜欢

转载自blog.csdn.net/xqt8888/article/details/82114440