Angular 6集成Spring Boot 2,Spring Security,JWT和CORS系列:五、heroapi项目中用Spring Security实现JWT令牌的身份认证

一、Spring Security基本原理

Spring Security在实现上是一系列过滤器,组成过滤器链,这些过滤器按一定的次序依次拦截请求,先是绿色的认证过滤器,再是蓝色的错误转换过滤器,再是橙色的安全拦截器,最后才是我们的接口。

Spring Security的身份认证,实际上是在其过滤器链的绿色区的某个节点上,根据一定的规则,构建一个认证Authentication,然后向SecurityContextHolder的当前上下文写入。

二、JWT令牌的身份认证过滤器
根据这个原理,我们编制一个过滤器,从jwt中解析出username和authorities,然后按照UsernamePasswordAuthenticationFilter中实现认证Authenticationde的方法,依次构造UserDetails、Authenticationde(UsernamePasswordAuthenticationToken),再把Authenticationde写入SecurityContextHolder即可。实现代码如下:

/**
 * 
 */
package com.jh.heroes.api.web.authentication;

import java.io.IOException;
import java.util.Collection;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import com.jh.heroes.api.exception.JwtAuthenticationException;
import com.jh.heroes.api.util.JwtHelper;

import io.jsonwebtoken.Claims;

/**
 * jwt认证过滤器
 * 
 * @author liangxh
 *
 */
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter{
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.web.filter.OncePerRequestFilter#doFilterInternal(javax.
	 * servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse,
	 * javax.servlet.FilterChain)
	 */
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws IOException, ServletException,JwtAuthenticationException {
		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			try {
				String authHeader = request.getHeader("Authorization");
				String tokenHead = "Bearer ";
	
				if (!StringUtils.isEmpty(authHeader) && authHeader.startsWith(tokenHead)) {
						String token = authHeader.substring(tokenHead.length());
						Claims claims = JwtHelper.parseJWT(token);
						if (claims != null) {
							String username = claims.get("username").toString();
							String role = claims.get("role").toString();
							String[] rolesArray = role.split(",");
							Collection<? extends GrantedAuthority> roles = Stream.of(rolesArray)
									.map(s->new SimpleGrantedAuthority(s)).collect(Collectors.toList());
							// 1、构建UserDetails
							UserDetails userDetails = new User(username, "N/A", roles);
							// 2、构建已经认证的令牌
							UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
									userDetails, "N/A", userDetails.getAuthorities());
							// 3、设置令牌的Details
							authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
							// 4、把令牌写入到当前安全线程上下文中:告知Spring Security过滤器链,当前线程已经认证,无需再认证
							SecurityContextHolder.getContext().setAuthentication(authentication);
						}
				}
			} catch (JwtAuthenticationException exception) {
				request.setAttribute("jwterror", exception.getMessage());
				response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(), exception.getMessage());
				return;
			}
		}
		filterChain.doFilter(request, response);
	}
	
}

 三、重构认证成功和失败处理器

重构的目的是返回统一的数据结构,便于前台解析。

1、增加统一数据返回

package com.jh.heroes.api.util;

import java.io.Serializable;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseMsg implements Serializable{

	/**
	 * 序列化.
	 */
	private static final long serialVersionUID = -8739271187671909578L;
	
	public static final String STATUS_SUCCESS = "0";
	public static final String STATUS_FAILED = "-1";
	
	/**
	 * 状态编码.
	 */
	private String status=STATUS_SUCCESS;
	/**返回消息.*/
	private String msg="";
	/**返回数据.*/
	private Object data;
	
	
	/**
	 * 失败.
	 * @param msg 失败原因
	 */
	public void failed(String msg)
	{
		this.setStatus(STATUS_FAILED);
		this.setMsg(msg);
	}
	
}

2、增加异常类

/**
 * 
 */
package com.jh.heroes.api.exception;

import org.springframework.security.core.AuthenticationException;

/**
 * JWT认证异常类.
 * @author liangxh
 *
 */
public class JwtAuthenticationException extends AuthenticationException {

	/**
	 * 
	 */
	private static final long serialVersionUID = 381802987164606243L;
	
	/**消息编码*/
	private String code = "";

	public String getCode() {
		return code;
	}
	public void setCode(String code) {
		this.code = code;
	}
	
	public JwtAuthenticationException(String msg) {
		super(msg);
	}
	
	public JwtAuthenticationException(String code,String msg) {
		super(msg);
		this.code=code;
	}

}

3、修改认证处理器

/**
 * 
 */
package com.jh.heroes.api.web.authentication;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.jh.heroes.api.util.JwtHelper;
import com.jh.heroes.api.util.ResponseMsg;

import lombok.extern.slf4j.Slf4j;

/**
 * 身份认证成功处理器
 * @author liangxh
 *
 */
@Slf4j
@Component("heroApiAuthenticationSuccessHandler")
public class HeroApiAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
	
	@Autowired
	private ObjectMapper objectMapper;
	

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.security.web.authentication.
	 * AuthenticationSuccessHandler#onAuthenticationSuccess(javax.servlet.http.
	 * HttpServletRequest, javax.servlet.http.HttpServletResponse,
	 * org.springframework.security.core.Authentication)
	 */
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		User user=(User)authentication.getPrincipal();
		log.info("登录成功:username="+user.getUsername()+" RemoteAddr="+request.getRemoteAddr()+" RemoteHost="+request.getRemoteHost()+" RemotePort="+request.getRemotePort());
		ResponseMsg msg=new ResponseMsg();
		msg.setData(JwtHelper.createJWT(user));
		response.setContentType("application/json;charset=UTF-8");
		response.getWriter().write(objectMapper.writeValueAsString(msg));
	}

}

4、修改认证失败处理器

/**
 * 
 */
package com.jh.heroes.api.web.authentication;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.jh.heroes.api.util.ResponseMsg;

import lombok.extern.slf4j.Slf4j;

/**
 * 身份认证失败处理器
 * 
 * @author liangxh
 *
 */
@Slf4j
@Component("heroApiAuthenctiationFailureHandler")
public class HeroApiAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
	
	@Autowired
	private ObjectMapper objectMapper;
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.security.web.authentication.AuthenticationFailureHandler#
	 * onAuthenticationFailure(javax.servlet.http.HttpServletRequest,
	 * javax.servlet.http.HttpServletResponse,
	 * org.springframework.security.core.AuthenticationException)
	 */
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		log.info("登录失败:{}");
		ResponseMsg msg=new ResponseMsg();
		msg.failed(exception.getMessage());
		response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
		response.setContentType("application/json;charset=UTF-8");
		response.getWriter().write(objectMapper.writeValueAsString(msg));
	}
}

四、增加访问无权限处理器

package com.jh.heroes.api.web.config;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.jh.heroes.api.util.ResponseMsg;

import lombok.extern.slf4j.Slf4j;

/**
 * 自定了权限不足的返回值
 * @author liangxh
 *
 */
@Slf4j
@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {
	
	@Autowired
	private ObjectMapper objectMapper;

	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response,
			AccessDeniedException accessDeniedException) throws IOException, ServletException {
		log.error(accessDeniedException.getMessage());
		ResponseMsg msg=new ResponseMsg();
		msg.failed("权限不足,不能访问");
		response.setStatus(HttpStatus.FORBIDDEN.value());
		response.setContentType("application/json;charset=UTF-8");
		response.getWriter().write(objectMapper.writeValueAsString(msg));
	}

}

五、增加未登录或认证不成功处理器

package com.jh.heroes.api.web.config;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.jh.heroes.api.util.ResponseMsg;

import lombok.extern.slf4j.Slf4j;

/**
 * 未登录或认证不成功处理器
 * @author liangxh
 * 我们没有使用form或basic等验证机制,需要自定义一个AuthenticationEntryPoint,当未验证用户访问受限资源时,返回401错误。如没有自定义AuthenticationEntryPoint,将返回403错误。使用方法见WebSecurityConfig。
 */
@Slf4j
@Component
public class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint {
	
	@Autowired
	private ObjectMapper objectMapper;

	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException authException) throws IOException, ServletException {
		// This is invoked when user tries to access a secured REST resource without supplying any credentials
        // We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
		log.info(authException.getMessage());
		ResponseMsg msg=new ResponseMsg();
		Object jwtErr=request.getAttribute("jwterror");
		if (jwtErr!=null) {
			msg.failed(jwtErr.toString());
			response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
		} else {
			msg.failed("请登录");
			response.setStatus(HttpStatus.UNAUTHORIZED.value());
		}
		response.setContentType("application/json;charset=UTF-8");
		response.getWriter().write(objectMapper.writeValueAsString(msg));
	}

}

注:在这个处理器中,判断是否存在jwterror(这个键取决于JWT令牌的身份认证过滤器),存在则说明在JWT令牌的身份认证过滤器中存在令牌错误,从而身份认证失败的。

六、修改WebSecurityConfig

package com.jh.heroes.api.web.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.jh.heroes.api.web.authentication.JsonUsernamePasswordAuthenticationFilter;
import com.jh.heroes.api.web.authentication.JwtAuthenticationTokenFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	
	/**
	 * 未登录或认证不成功处理器
	 */
	@Autowired
    private EntryPointUnauthorizedHandler unauthorizedHandler;
	
	/**
	 * 访问无权限处理器
	 */
	@Autowired
    private AccessDeniedHandler accessDeniedHandler;
	
	/**
	 * 密码编码、解码器
	 */
	@Autowired
	private PasswordEncoder passwordEncoder;
	
	/**
	 * 用户
	 */
	@Autowired
    private UserDetailsService userDetailsService;
	
	/**
	 * 登录成功处理器
	 */
	@Autowired
    private AuthenticationSuccessHandler heroApiAuthenticationSuccessHandler;
    
    /**
     * 登录失败处理器
     */
    @Autowired
    private AuthenticationFailureHandler heroApiAuthenctiationFailureHandler;
    

    
    /* (non-Javadoc)
     * @see org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#authenticationManagerBean()
     */
    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
    	//解决 http.getSharedObject(AuthenticationManager.class) 无法获取AuthenticationManager实例
        return super.authenticationManagerBean();
    }
    
    @Autowired
    protected AuthenticationManager authenticationManager;
    
	@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordAuthenticationFilter=new JsonUsernamePasswordAuthenticationFilter();
		jsonUsernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager);
		jsonUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(heroApiAuthenticationSuccessHandler);
		jsonUsernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(heroApiAuthenctiationFailureHandler);
		
		JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter=new JwtAuthenticationTokenFilter();

		http
		.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
		.addFilterAfter(jsonUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
		.cors()
		.and()
		.csrf().disable()
		.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).accessDeniedHandler(accessDeniedHandler)
		.and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // don't create session
		.and()
		.authorizeRequests()
		.antMatchers("/jsonlogin").permitAll()
		.antMatchers("/login").permitAll()
		.anyRequest()
		.authenticated();
	}
}

这次的配置文件主要存在一下修改:

1、取消formlogin认证模式,避免formLogin认证中的页面跳转等等。

2、取消session支持。

3、注入认证不成功和访问权限不足处理器。

4、把jwtAuthenticationTokenFilter注入到UsernamePasswordAuthenticationFilter的实例之前。

七、小结

1、充分利用Spring Security基本原理,通过过滤器解析jwt实现身份模拟认证;并且通过jwt实现自认证,不访问数据库。

2、取消formlogin认证模式,增加认证不成功和访问权限不足处理器,便于前后端分离模式。

3、统一数据返回,便于前台解析数据。

八、参考

1、JWT+Redis+Spring Security 实现无状态化认证

2、Spring Security + JWT 实现基于Token的安全验证

3、Spring Boot实战之Filter实现使用JWT进行接口认证

4、Angular 6集成Spring Boot 2,Spring Security,JWT和CORS

5、微服务架构中的身份验证问题 :JSON Web Tokens( JWT)

猜你喜欢

转载自blog.csdn.net/lxhjh/article/details/82353776