SpringSecurity+jwt,实现前后分离 适配Resful API的权限控制

前言

SpringSecurity默认采用的基于表单的认证形式,以session识别 从用户登录授权、鉴权等都与表单相关。而当前许多应用都采用SpringBoot 基于Resful API风格的开发,使用默认的SpringSecurity无法直接使用满足。解决方案:不采用默认的登录鉴权方式,而通过覆写增加其中的过滤器Filter来实现 登录和鉴权,并将JWT(JSON WEB TOKEN)作为认证机制。

原理:

  1. 首次使用username+password请求登录接口
  2. 验证成功后向server生成并向client返回一个定制化jwt
  3. 后续clinet的每次HTTP请求都携带 header=Authorization, value=jwt
  4. server接收到请求将获取jwt中的验证信息,生成一个在SpringSecurity认证体系中的UsernamePasswordAuthenticationToken (此Token与返给客户端的jwt不同)

实现步骤:

  • JWTLoginFilter implement UsernamePasswordAuthenticationFilter (登录过滤器)
  • JWTAuthenticationFilter extends BasicAuthenticationFilter (token检验过滤器)
  • JWTUtils (操作jwt的工具类)

源码地址:https://github.com/YoungerJam/SpringSecurityWithJwtDemo

重点

// JWTLoginFilter.java

package com.SpringSecurityWithJwt.demo.filter;

import com.SpringSecurityWithJwt.demo.entity.User;
import com.SpringSecurityWithJwt.demo.utils.JwtUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;

/**
 * 登录过滤器 client发送POST请求到此验证
 * 验证成功后将生成返回token给client
 *
 * @Author: Jam
 * @Date: 2020/3/12 17:24
 */
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {


    private AuthenticationManager authenticationManager;

    /**
     * 将登录请求的路径设置为 /auth (默认下的 /login)
     * @param authenticationManager 认证管理
     */
    public JWTLoginFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        super.setFilterProcessesUrl("/auth");
    }

    /**
     * 重点覆写的方法,方法名直接译 尝试认证
     * @param request 携带 用户名+密码 的请求体
     * @param response
     * @return 认证信息
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            User user = new User();
            user.setUsername(request.getParameter("username"));
            user.setPassword(request.getParameter("password"));
            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
                    user.getUsername(),
                    user.getPassword(),
                    new ArrayList<>()
            ));
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 覆写成功认证的方法,若账号密码校验成功,会调用到此方法
     * 并生成对应的token返回给client
     * @param request 
     * @param response 返回给client的响应体,(会在头部加上token认证令牌,此后的访问需携带此token)
     * @param chain
     * @param auth
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,Authentication auth)throws  IOException, ServletException{
        Collection<? extends GrantedAuthority> authorities=auth.getAuthorities();
        String username=((org.springframework.security.core.userdetails.User) auth.getPrincipal()).getUsername();
        String role="";
        for(GrantedAuthority authority:authorities){
            role=authority.getAuthority();
        }
        System.out.println(username+" "+role);
        String token=JwtUtils.createToken(username,role);
        response.addHeader(JwtUtils.TOKEN_HEADER, JwtUtils.TOKEN_PREFIX+token);
    }
}

// JWTAuthenticationFilter.java

package com.SpringSecurityWithJwt.demo.filter;

import com.SpringSecurityWithJwt.demo.utils.JwtUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;

/**
 * token校验过滤器
 * 携带token的http请求将会在此类进行token校验
 * jwt提供了token检验机制
 *
 * @Author: Jam
 * @Date: 2020/3/12 18:00
 */

public class JWTAuthenticationFilter extends BasicAuthenticationFilter {

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) {
        super(authenticationManager, authenticationEntryPoint);
    }

    /**
     * 覆写方法
     * @param request 携带token的请求体
     * @param response
     * @param chain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        String header=request.getHeader(JwtUtils.TOKEN_HEADER);
        //过滤没有token的(可能是无需授权的访问)
        if(header==null||!header.startsWith(JwtUtils.TOKEN_PREFIX)){
            chain.doFilter(request,response);
            return;
        }
        UsernamePasswordAuthenticationToken authentication = getAuthentication(header);
        //设置该用户的认证信息,由jwtToken生成UsernamePasswordAuthenticationToken
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(request, response);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(String header) {
        String token = header.replace(JwtUtils.TOKEN_PREFIX, "");
        String username = JwtUtils.getUsername(token);
        String role = JwtUtils.getUserRole(token);
        System.out.println("授权:"+username+" "+role);
        if (username != null) {
            return new UsernamePasswordAuthenticationToken(username, null,
                    Collections.singleton(new SimpleGrantedAuthority(role))
            );
        }
        return null;
    }

}

// JwtUtils.java

package com.SpringSecurityWithJwt.demo.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author: Jam
 * @Date: 2020/3/13 0:36
 */
public class JwtUtils {
    public static final String TOKEN_HEADER="Authorization";
    public static final String TOKEN_PREFIX="Bearer ";
    private static final String SECRET="MyJwtSecret";
    private static final long EXPIRATION=7200;
    private static final String ROLE_CLAIMS="rol";

    public static String createToken(String username,String role){
        Map<String,Object> map=new HashMap<>();
        map.put(ROLE_CLAIMS,role);
        return Jwts.builder()
                .signWith(SignatureAlgorithm.HS512,SECRET)
                .setClaims(map)
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis()+EXPIRATION*1000))
                .compact();
    }
    public static String getUsername(String token){
        return getTokenBody(token).getSubject();
    }
    public static String getUserRole(String token){
        return (String) getTokenBody(token).get(ROLE_CLAIMS);
    }
   
    //解析jwt
    private static Claims getTokenBody(String token){
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody();
    }
}

// JWTAuthenticationEntryPoint 异常处理类

package com.SpringSecurityWithJwt.demo.exception;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Author: Jam
 * @Date: 2020/3/13 15:09
 */
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.getWriter().write(new ObjectMapper().writeValueAsString("无访问权限 "+authException.getMessage()));
    }
}

// SecurityConfig.java

package com.SpringSecurityWithJwt.demo.config;

import com.SpringSecurityWithJwt.demo.exception.JWTAuthenticationEntryPoint;
import com.SpringSecurityWithJwt.demo.filter.JWTAuthenticationFilter;
import com.SpringSecurityWithJwt.demo.filter.JWTLoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import javax.sql.DataSource;

/**
 * @Author: Jam
 * @Date: 2020/3/9 16:31
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final DataSource dataSource;

    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @Autowired
    public SecurityConfig(DataSource dataSource, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.dataSource = dataSource;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable().authorizeRequests()
                .antMatchers(HttpMethod.GET, "/user/query").authenticated()
                .antMatchers(HttpMethod.POST, "/user/register").permitAll()
                .antMatchers(HttpMethod.POST, "/user/update").hasRole("USER")
                .anyRequest().authenticated()
                .and()
                .addFilter(new JWTLoginFilter(authenticationManager()))
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint());
    }

    /**
    * 这里我采用的是jdbc的数据库表认证
    **/
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .jdbcAuthentication()
                .dataSource(dataSource)
                .usersByUsernameQuery(
                        "select username,password,true from t_user where username=?"
                )
                .authoritiesByUsernameQuery(
                        "select username,'ROLE_USER' from t_user where username=?"

                )
                .passwordEncoder(new BCryptPasswordEncoder());
    }
}

测试

  1. 开放接口 /user/register
    在这里插入图片描述
    开放接口能正常调用,注意为post请求 在配置时设置了只有 post方法下的该路径才方通,而 get /user/registrer 则属于配置.anyRequest().authenticated()
    所以如果手误以get 请求该路径,会返回无授权访问的信息
    在这里插入图片描述
  2. 登录接口 /auth
    在这里插入图片描述
    由于没有覆写成功的返回体信息,所以Reponse中的body是空的,有需要可以自行添加,重点关注reponse Header中的Authorization 这个头部即为服务器返回的 jwt token。
  3. GET /user/query 和 POST /user/update接口测试
    在这里插入图片描述
    不携带token时响应如上,在Header中加上刚刚登录时返回的Authorzation
    在这里插入图片描述
    在这里插入图片描述
    测试完成,权限控制正常发挥作用,返回idea可以看到刚刚处理过程中的一些权限相关信息 (我在两个FIlter里写的sout)
    在这里插入图片描述

剖析

实现过程中,通过继承UsernamePasswordAuthenticationFilter覆写了attemptAuthenticaton和successfulAuthentication方法,来看看源码中的该类
(截取重点部分)
在这里插入图片描述
只接接受 /login路径下的post请求 原则是post请求不可更改,而路径可以随意设置 如上述代码中的 super.setFilterProcessesUrl("/auth");
在这里插入图片描述
在这里插入图片描述
这里可以看到 其实在源码实现层面上,内部使用的便是 UsernamePasswordAuthenticationToken 这也是为什么我们在携带jwt再次请求的时候要解析jwt生成 APAT(英文简写吧),所以不需要调到attempt方法但仍然可以具备授权身份。
在这里插入图片描述
// 截留了源码中的关键语句


	private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
	private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
successHandler.onAuthenticationSuccess(request, response, authResult);
	}

验证成功的处理方法,处于AbstractAuthenticat… Filter中,有兴趣可以自己调出来看看,默认是调用Handler类处理后续,所以如果选择不覆写此方法的话可以自己去是写一个实现Hadnler类也能达到想要的自定义登录成功/失败处理。

再来看看JWTAuthentication的父类 BsicAuthenticationFitler
点开后 - - 有很大一篇的备注,看看重点
在这里插入图片描述我们只需要关注两个关键点
1、我们携带token的请求被谁拦截处理(如上图)所以我们可以扩展它
2、怎么处理这个token 并完成识别用户权限。

在上面我们提到了
SecurityContextHolder.getContext().setAuthentication(authResult);
这个语句便是设置认证信息的关键所在,再来会看一下这个频繁提到的UsernamePasswordAuthenticationToken到底是怎样的数据结构
该类有个主体字段 principal
输出一下验证账号密码后生成的UPAT看看

org.springframework.security.core.userdetails.User@a0ccb02b: 
Username: 771007760;
Password: [PROTECTED]; 
Enabled: true; 
AccountNonExpired: true; 
credentialsNonExpired: true;
AccountNonLocked: true; 
Granted Authorities: ROLE_USER

几乎囊括了用户的的所有信息。该类的一个构造器(上述在JWTAuthenticationFilter调用的构造器)

在这里插入图片描述
我们携带的JwtToken解析生成回 UPAT,只需写入username,null,role 便可完成认证。
输出一下通过token解析反生成的UPAT看看

   System.out.println(u.getPrincipal()+" "+u.getAuthorities());

771007760 [ROLE_USER]
没有其他的信息,因为此时这就具备了鉴权条件了 username+role

参考了多方文档与实现方案 也没办法标出全部的借鉴,所以在此感谢所有大佬吧。

源码地址:https://github.com/YoungerJam/SpringSecurityWithJwtDemo

发布了7 篇原创文章 · 获赞 7 · 访问量 1846

猜你喜欢

转载自blog.csdn.net/TheJam/article/details/104844205