SpringBoot 整合 Spring Security 、JWT 实现认证、权限控制

此文章用到的版本

spring-boot : 2.6.8
java 1.8

引入依赖包(gradle) maven 请自行转换


dependencies {
    compile group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

先说说原理

UsernamePasswordAuthenticationFilter 继承 AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter.doFilter() 会执行 抽象方法attemptAuthentication ()

通过观察发现 UsernamePasswordAuthenticationFilter 会拦截 POST /login 的请求

然后通过会通过Http parameter 获取 username 和 password 参数的值执行鉴权认证

	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";

	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

	private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
			"POST");

	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;

	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

	private boolean postOnly = true;

	public UsernamePasswordAuthenticationFilter() {
		super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
	}

	public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
		super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
	}

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		if (this.postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		String username = obtainUsername(request);
		username = (username != null) ? username : "";
		username = username.trim();
		String password = obtainPassword(request);
		password = (password != null) ? password : "";
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		return this.getAuthenticationManager().authenticate(authRequest);
	}

	@Nullable
	protected String obtainUsername(HttpServletRequest request) {
		return request.getParameter(this.usernameParameter);
	}

成功会执行 SavedRequestAwareAuthenticationSuccessHandler  重定向到指定url

public class SavedRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

	protected final Log logger = LogFactory.getLog(this.getClass());

	private RequestCache requestCache = new HttpSessionRequestCache();

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws ServletException, IOException {
		SavedRequest savedRequest = this.requestCache.getRequest(request, response);
		if (savedRequest == null) {
			super.onAuthenticationSuccess(request, response, authentication);
			return;
		}
		String targetUrlParameter = getTargetUrlParameter();
		if (isAlwaysUseDefaultTargetUrl()
				|| (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
			this.requestCache.removeRequest(request, response);
			super.onAuthenticationSuccess(request, response, authentication);
			return;
		}
		clearAuthenticationAttributes(request);
		// Use the DefaultSavedRequest URL
		String targetUrl = savedRequest.getRedirectUrl();
		getRedirectStrategy().sendRedirect(request, response, targetUrl);
	}

	public void setRequestCache(RequestCache requestCache) {
		this.requestCache = requestCache;
	}

}

失败会执行  SimpleUrlAuthenticationFailureHandler

	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		if (this.defaultFailureUrl == null) {
			if (this.logger.isTraceEnabled()) {
				this.logger.trace("Sending 401 Unauthorized error since no failure URL is set");
			}
			else {
				this.logger.debug("Sending 401 Unauthorized error");
			}
			response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
			return;
		}
		saveException(request, exception);
		if (this.forwardToDestination) {
			this.logger.debug("Forwarding to " + this.defaultFailureUrl);
			request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response);
		}
		else {
			this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl);
		}
	}

这种方式不兼容json方式提交的登录 而且不能返回token 供前端使用 所以我们需要改造此Filter

实现思路:

1.  拦截Post /login 请求

2.  获取请求中的body参数 username 以及password 

3. 返回 UsernamePasswordAuthenticationFilter 不携带权限集合

4. 重写 UserDetailsService 查询数据库

5. 重写 AuthenticationSuccessHandler 登录成功后返回jwt token令牌

6. 重写 AuthenticationFailureHandler 失败返回失败原因 例如:密码错误,账户锁定, 账户不存在

定义token常量

public class SecurityConstants
{
    public static final long EXPIRATION_TIME = 864_000_000; // 10 days
    public static final String TOKEN_PREFIX = "Bearer ";
    public static final String HEADER_STRING = "Authorization";

    private SecurityConstants()
    {
        throw new IllegalStateException("Utility class");
    }
}

实现工具类 JWTUtils 用户生成、解析token

@Component
public class JwtUtil
{

    /**
     * 签名用的密钥
     */
    private static String signKey = "nacl";


    /**
     * 用户登录成功后生成Jwt
     * 使用Hs256算法
     *
     * @param exp jwt过期时间
     * @param claims 保存在Payload(有效载荷)中的内容
     * @return token字符串
     */
    public static String createJWT(Date exp, Map<String, Object> claims)
    {
        //指定签名的时候使用的签名算法
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        //生成JWT的时间
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        //创建一个JwtBuilder,设置jwt的body
        JwtBuilder builder = Jwts.builder()
            //保存在Payload(有效载荷)中的内容
            .setClaims(claims)
            //iat: jwt的签发时间
            .setIssuedAt(now)
            //设置过期时间
            .setExpiration(exp)
            //设置签名使用的签名算法和签名使用的秘钥
            .signWith(signatureAlgorithm, signKey);

        return builder.compact();
    }

    /**
     * 解析token,获取到Payload(有效载荷)中的内容,包括验证签名,判断是否过期
     *
     * @param token
     * @return
     */
    public static Claims parseJWT(String token)
    {
        //得到DefaultJwtParser
        return Jwts.parser()
            //设置签名的秘钥
            .setSigningKey(signKey)
            //设置需要解析的token
            .parseClaimsJws(token).getBody();
    }

}

开始实现身份认证过滤器(JWTAuthenticationFilter) 继承 UsernamePasswordAuthenticationFilter  重写 attemptAuthentication 方法

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter
{


    @Override
    public Authentication attemptAuthentication(HttpServletRequest req,
                                                HttpServletResponse res)
    {
        Map<String, String> creds = new HashMap<>();
        try
        {
            creds = new ObjectMapper().readValue(req.getInputStream(), Map.class); // 获取body中的参数
        } catch (IOException e)
        {
            e.printStackTrace();
        }
        return this.getAuthenticationManager().authenticate(
            new UsernamePasswordAuthenticationToken(
                creds.get("username"),
                creds.get("password"),
                new ArrayList<>())
        );
    }
}

重写UserDetailService

返回一个测试用户 用户名:123 密码:123 角色权限: ROLE_ADMIN

@Service
public class UserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService
{

    @Override
    public UserDetails loadUserByUsername(String username)
    {
        Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); // 设定权限
        return new User(
            "123", // username
            new BCryptPasswordEncoder().encode("123") , // password
            true, // enabled – set to true if the user is enabled
            true, // accountNonExpired – set to true if the account has not expired
            true, // credentialsNonExpired – set to true if the credentials have not expired
            true, // accountNonLocked – set to true if the account is not locked
            authorities // authorities – the authorities that should be granted to the caller if they presented the
        );
    }
}

重写 AuthenticationSuccessHandler 成功后将生成的 token 放入 response header

@Component
public class CustomAuthenticateSuccessHandler implements AuthenticationSuccessHandler
{

    @Override
    public void onAuthenticationSuccess(
        HttpServletRequest request,
        HttpServletResponse response,
        Authentication auth) throws IOException, ServletException
    {
        Map<String, Object> claims = new HashMap<>();
        claims.put("username", ((User) auth.getPrincipal()).getUsername());

        String token = JwtUtil.createJWT(
            new Date(System.currentTimeMillis() + SecurityConstants.EXPIRATION_TIME),
            claims
        );

        response.addHeader(SecurityConstants.HEADER_STRING, SecurityConstants.TOKEN_PREFIX + token);
    }
}

重写 AuthenticationFailureHandler 返回账户失败原因

@Component
public class CustomAuthenticateFailureHandler implements AuthenticationFailureHandler
{
    @Override
    public void onAuthenticationFailure(
        HttpServletRequest request,
        HttpServletResponse response,
        AuthenticationException failed
    ) throws IOException, ServletException
    {
        String returnData = "";
        // 账号过期
        if (failed instanceof AccountExpiredException)
        {
            returnData = "账号过期";
        }
        // 密码错误
        else if (failed instanceof BadCredentialsException)
        {
            returnData = "密码错误";
        }
        // 密码过期
        else if (failed instanceof CredentialsExpiredException)
        {
            returnData = "密码过期";
        }
        // 账号不可用
        else if (failed instanceof DisabledException)
        {
            returnData = "账号不可用";
        }
        //账号锁定
        else if (failed instanceof LockedException)
        {
            returnData = "账号锁定";
        }
        // 用户不存在
        else if (failed instanceof InternalAuthenticationServiceException)
        {
            returnData = "用户不存在";
        }
        // 其他错误
        else
        {
            returnData = "未知异常";
        }

        // 处理编码方式 防止中文乱码
        response.setContentType("text/json;charset=utf-8");
        // 将反馈塞到HttpServletResponse中返回给前台
        response.getWriter().write(returnData);
    }
}


 

改造BasicAuthenticationFilter基于JWT解析 实现权限认证

认证过滤器 BasicAuthenticationFilter

header里头有Authorization,而且value是以Basic开头的,则走BasicAuthenticationFilter,提取参数构造UsernamePasswordAuthenticationToken进行认证,成功则填充SecurityContextHolder的Authentication

而我们要做的是 header里头有Authorization,而且value是以Bearer开头的, 解析jwt token填充SecurityContextHolder的Authentication

@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		try {
			UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request);
			if (authRequest == null) {
				this.logger.trace("Did not process authentication request since failed to find "
						+ "username and password in Basic Authorization header");
				chain.doFilter(request, response);
				return;
			}
			String username = authRequest.getName();
			this.logger.trace(LogMessage.format("Found username '%s' in Basic Authorization header", username));
			if (authenticationIsRequired(username)) {
				Authentication authResult = this.authenticationManager.authenticate(authRequest);
				SecurityContextHolder.getContext().setAuthentication(authResult);
				if (this.logger.isDebugEnabled()) {
					this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
				}
				this.rememberMeServices.loginSuccess(request, response, authResult);
				onSuccessfulAuthentication(request, response, authResult);
			}
		}
		catch (AuthenticationException ex) {
			SecurityContextHolder.clearContext();
			this.logger.debug("Failed to process authentication request", ex);
			this.rememberMeServices.loginFail(request, response);
			onUnsuccessfulAuthentication(request, response, ex);
			if (this.ignoreFailure) {
				chain.doFilter(request, response);
			}
			else {
				this.authenticationEntryPoint.commence(request, response, ex);
			}
			return;
		}

		chain.doFilter(request, response);
	}

@Override
	public UsernamePasswordAuthenticationToken convert(HttpServletRequest request) {
		String header = request.getHeader(HttpHeaders.AUTHORIZATION);
		if (header == null) {
			return null;
		}
		header = header.trim();
		if (!StringUtils.startsWithIgnoreCase(header, AUTHENTICATION_SCHEME_BASIC)) {
			return null;
		}
		if (header.equalsIgnoreCase(AUTHENTICATION_SCHEME_BASIC)) {
			throw new BadCredentialsException("Empty basic authentication token");
		}
		byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
		byte[] decoded = decode(base64Token);
		String token = new String(decoded, getCredentialsCharset(request));
		int delim = token.indexOf(":");
		if (delim == -1) {
			throw new BadCredentialsException("Invalid basic authentication token");
		}
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(token.substring(0, delim),
				token.substring(delim + 1));
		result.setDetails(this.authenticationDetailsSource.buildDetails(request));
		return result;
	}

开始实现 继承BasicAuthenticationFilter 并重写 doFilterInternal 方法

getAuthentication 获取 request header中的token 解析成username 并调用Userservice中的loadUserByName方法返回User鉴权信息

装入SecurityContextHolder

public class JWTAuthorizationFilter extends BasicAuthenticationFilter
{

    public JWTAuthorizationFilter(AuthenticationManager authManager)
    {
        super(authManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                    HttpServletResponse res,
                                    FilterChain chain) throws IOException, ServletException
    {
        String header = req.getHeader(SecurityConstants.HEADER_STRING);

        if (header == null || !header.startsWith(SecurityConstants.TOKEN_PREFIX))
        {
            chain.doFilter(req, res);
            return;
        }
        try
        {
            UsernamePasswordAuthenticationToken authentication = getAuthentication(req);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(req, res);
        } catch (ExpiredJwtException e)
        {
            res.getWriter().write("token expired");
        } catch (JwtException e)
        {
            res.getWriter().write("token invalid");
        }

    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request)
    {
        String token = request.getHeader(SecurityConstants.HEADER_STRING);
        if (token != null)
        {
            // parse the token.
            Claims claims = JwtUtil.parseJWT(token.replace(SecurityConstants.TOKEN_PREFIX, ""));

            if (claims != null)
            {
                UserDetailsService userDetailsService = new UserDetailsService();
                User u = (User) userDetailsService.loadUserByUsername((String) claims.get("username"));
                return new UsernamePasswordAuthenticationToken(u.getUsername(), u.getPassword(), u.getAuthorities());
            }
        }
        return null;
    }
}
CustomAccessDeniedHandler 非匿名下的错误拦截器
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler
{
    @Override
    public void handle(
        HttpServletRequest request,
        HttpServletResponse response,
        AccessDeniedException accessDeniedException) throws IOException, ServletException
    {
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write("权限错误");
    }
}
CustomAuthenticationEntryPoint 匿名下的错误拦截器
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint
{
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException
    {
        response.getWriter().write("no login");
    }
}

OK 准备大功告成, 最后设定一下Spring Security的配置

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

    private final UserDetailsService userDetailsService;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    private final CustomAccessDeniedHandler customAccessDeniedHandler;
    private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
    private final CustomAuthenticateSuccessHandler customAuthenticateSuccessHandler;
    private final CustomAuthenticateFailureHandler customAuthenticateFailureHandler;

    public SpringSecurityConfig(
        UserDetailsService userDetailsService,
        BCryptPasswordEncoder bCryptPasswordEncoder,
        CustomAccessDeniedHandler customAccessDeniedHandler,
        CustomAuthenticationEntryPoint customAuthenticationEntryPoint,
        CustomAuthenticateSuccessHandler customAuthenticateSuccessHandler,
        CustomAuthenticateFailureHandler customAuthenticateFailureHandler
    )
    {
        this.userDetailsService = userDetailsService;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
        this.customAccessDeniedHandler = customAccessDeniedHandler;
        this.customAuthenticationEntryPoint = customAuthenticationEntryPoint;
        this.customAuthenticateFailureHandler = customAuthenticateFailureHandler;
        this.customAuthenticateSuccessHandler = customAuthenticateSuccessHandler;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception
    {
        http
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and().csrf().disable()
            .authorizeRequests().antMatchers("/sign-up").permitAll()
            .anyRequest().authenticated()
            .and()
            .exceptionHandling()
            .accessDeniedHandler(customAccessDeniedHandler)
            .authenticationEntryPoint(customAuthenticationEntryPoint)
            .and()
            .addFilter(jwtAuthenticationFilter())
            .addFilter(jwtAuthorizationFilter());
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
    }

    @Bean
    public JWTAuthenticationFilter jwtAuthenticationFilter() throws Exception
    {
        JWTAuthenticationFilter filter = new JWTAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManager());
        filter.setAuthenticationSuccessHandler(customAuthenticateSuccessHandler);
        filter.setAuthenticationFailureHandler(customAuthenticateFailureHandler);
        return filter;
    }

    @Bean
    public JWTAuthorizationFilter jwtAuthorizationFilter () throws Exception
    {
        return new JWTAuthorizationFilter(authenticationManager());
    }
}

编写测试Controller

@RestController
public class TestController
{
    @PostMapping("/sign-up")
    public String signUp ()
    {
        return "1111";
    }

    @PostMapping("/admin")
    @PreAuthorize("hasRole('ADMIN')")
    public String admin ()
    {
        return "222";
    }

    @PostMapping("/user")
    @PreAuthorize("hasRole('USER')")
    public String user ()
    {
        return "333";
    }
}

测试 /login 登录

输入错误的账号密码

输入正确的账号密码

放行请求/sign-up 

身份鉴权认证

无token

错误token

正确token 无权限

正确token 有权限

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值