一琼娘、spring boot Security+JWT 在Spring Cloud網(wǎng)關(guān)層實(shí)現(xiàn)用戶(hù)認(rèn)證
1、引入Spring Security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、創(chuàng)建WebSecurityConfig繼承WebSecurityConfigurerAdapter
重寫(xiě)configure(HttpSecurity http)方法。WebSecurityConfigurerAdapter是由Spring Security提供的Web應(yīng)用安全配置的適配器竟秫。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationTokenFilter();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 由于使用的是JWT愕提,我們這里不需要csrf
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// 對(duì)于獲取token的rest api要允許匿名訪問(wèn)
.antMatchers("/api-user/safeVerify/**").permitAll()
.antMatchers("/api-base/manage/userLogin/**").permitAll()
.antMatchers("/api-user/app/login").permitAll()
.antMatchers("/api-user/app/version").permitAll()
.antMatchers("/api-user/app/refresh/token").permitAll()
.antMatchers("/swagger-ui.html/**", "/swagger-resources/**", "/*/v2/api-docs/**").permitAll()//swagger文檔無(wú)授權(quán)訪問(wèn)
// 除上面外的所有請(qǐng)求全部需要鑒權(quán)認(rèn)證
.anyRequest().authenticated();
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
// 禁用緩存
httpSecurity.headers().cacheControl();
}
}
Spring Security包含了眾多的過(guò)濾器,這些過(guò)濾器形成了一條鏈脖祈,所有請(qǐng)求都必須通過(guò)這些過(guò)濾器后才能成功訪問(wèn)到資源喻奥。其中UsernamePasswordAuthenticationFilter過(guò)濾器用于處理基于表單方式的登錄認(rèn)證过牙。我們通過(guò)該過(guò)濾器,實(shí)現(xiàn)JWT的用戶(hù)認(rèn)證。
3蚁袭、創(chuàng)建JWTAuthenticationTokenFilter 過(guò)濾器鬼悠,實(shí)現(xiàn)對(duì)請(qǐng)求token的用戶(hù)認(rèn)證
@SuppressWarnings("SpringJavaAutowiringInspection")
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private RedisUtil redisUtil;
@Autowired
private UserApi userApi;
private static final String ERP_HEADER = "authorization-erp-fqkj";
private static final String FILTER_APPLIED = "__spring_security_Filter_filterApplied";
private static final String HEADER_USER = "key_userinfo_in_http_header";
private static final String TOKEN_EXPRIED = "Filter_TokenExpried";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
if (request.getMethod().equals("OPTIONS")) {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Content-Type", "application/json");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with,Content-Type,authorization-erp-fqkj,authorization-app-fqkj,authorization-manage-fqkj");
response.setStatus(HttpServletResponse.SC_OK);
return;
}
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, true);
AppendHeaderRequestWrapper requestWrapper = new AppendHeaderRequestWrapper(request);
String authToken = request.getHeader(ERP_HEADER);
if (authToken != null) {
String userSubject = jwtTokenUtil.getUserIdFromToken(authToken);
boolean isExpired = jwtTokenUtil.isTokenExpired(authToken);
if(!isExpired){
setUserSecurityContext(userSubject, requestWrapper);
}
}
chain.doFilter(requestWrapper, response);
}
private void setUserSecurityContext(String userSubject, AppendHeaderRequestWrapper requestWrapper) {
String companyId = userSubject.split("#")[0];
String userId = userSubject.split("#")[1];
String key = companyId + RedisSuffixConstants.LOGIN_USERLIST;
boolean isExist = redisUtil.hHasKey(key, userId);
UserInfo userInfo;
if (isExist) {
userInfo = redisUtil.hget(key, userId);
} else {
//如果redis中不存在
userInfo = userApi.getUserInfoByUserId(userId);
}
if (userInfo == null) {
return;
}
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userSubject);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(requestWrapper));
SecurityContextHolder.getContext().setAuthentication(authentication);
redisUtil.hset(key, userId, userInfo);
UserInfoContext.setUser(userInfo);
String userJson = JSON.toJSONString(userInfo);
try {
requestWrapper.putHeader(HEADER_USER, URLDecoder.decode(userJson, "UTF-8"));
} catch (UnsupportedEncodingException e) {
log.error("init userInfo error", e);
}
}
}
該過(guò)濾器的處理步驟:
1)判斷httpmothod為“OPTIONS”,直接放通,允許跨域訪問(wèn)。
2)判斷請(qǐng)求header中是否存在指定名的token,調(diào)用JWT工具類(lèi)獲取當(dāng)前用戶(hù)標(biāo)識(shí)缎讼,判斷token是否過(guò)期笛辟。
3)根據(jù)用戶(hù)標(biāo)識(shí)從redis中獲取當(dāng)前用戶(hù)的基本信息围来,沒(méi)有調(diào)用會(huì)員服務(wù)獲取用戶(hù)信息航唆。并更新redis任岸。
4) 生成UsernamePasswordAuthenticationToken嗅蔬,保存到SecurityContext中。
5)把用戶(hù)基本信息ToJson為文本,保存到請(qǐng)求頭中漆诽。
4供鸠、包裝當(dāng)前請(qǐng)求類(lèi)HttpServletRequestWrapper,在當(dāng)前請(qǐng)求頭中加入登錄用戶(hù)的基本信息
public class AppendHeaderRequestWrapper extends HttpServletRequestWrapper {
private final Map<String, String> customHeaders;
public AppendHeaderRequestWrapper(HttpServletRequest request) {
super(request);
this.customHeaders = new HashMap<>();
}
void putHeader(String name, String value){
this.customHeaders.put(name, value);
}
@Override
public String getHeader(String name) {
// check the custom headers first
String headerValue = customHeaders.get(name);
if (headerValue != null){
return headerValue;
}
// else return from into the original wrapped object
return ((HttpServletRequest) getRequest()).getHeader(name);
}
@Override
public Enumeration<String> getHeaderNames() {
// create a set of the custom header names
Set<String> set = new HashSet<>(customHeaders.keySet());
// now add the headers from the wrapped request object
Enumeration<String> e = ((HttpServletRequest) getRequest()).getHeaderNames();
while (e.hasMoreElements()) {
// add the names of the request headers into the list
String n = e.nextElement();
set.add(n);
}
// create an enumeration from the set and return
return Collections.enumeration(set);
}
}
5乡数、定義驗(yàn)證失敗后的處理類(lèi)
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final String TOKEN_EXPRIED = "Filter_TokenExpried";
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
ResultVO resultVO;
if (httpServletRequest.getAttribute(TOKEN_EXPRIED) != null) {
resultVO=ResultVO.fail(CoreConstants.TOKEN_EXPIRED);
}else {
resultVO=ResultVO.fail(CoreConstants.NEED_AUTHORITIES);
}
String resultJson = URLDecoder.decode(JSON.toJSONString(resultVO), "UTF-8");
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
httpServletResponse.setHeader("Content-Type", "application/json;charset=UTF-8");
httpServletResponse.setHeader("Access-Control-Allow-Headers", "x-requested-with,authorization-erp-fqkj,authorization-app-fqkj,authorization-manage-fqkj");
httpServletResponse.getWriter().write(resultJson);
}
}
二烧栋、在Spring Cloud網(wǎng)關(guān)中魔吐,通過(guò)ZuulFilter辞色,把當(dāng)前登錄用戶(hù)的基本信息注入到請(qǐng)求頭中
@Component
public class ZuulAccessFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(ZuulAccessFilter.class);
private static final String HEADER_USER = "key_userinfo_in_http_header";
@Override
public String filterType() {
//前置過(guò)濾器
return "pre";
}
@Override
public int filterOrder() {
//優(yōu)先級(jí)立美,數(shù)字越大,優(yōu)先級(jí)越低
return 0;
}
@Override
public boolean shouldFilter() {
//是否執(zhí)行該過(guò)濾器蔫巩,true代表需要過(guò)濾
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String userInfoStr = request.getHeader(HEADER_USER);
if (!Strings.isNullOrEmpty(userInfoStr)) {
try {
ctx.addZuulRequestHeader(HEADER_USER, URLEncoder.encode(userInfoStr, "UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
return ctx;
}
}
三、在Spring Cloud微服務(wù)中,定義過(guò)濾器贾陷,解析網(wǎng)關(guān)傳遞的請(qǐng)求頭,解析出當(dāng)前訪問(wèn)用戶(hù)的基本信息
public class TransmitUserInfoFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(TransmitUserInfoFeighClientIntercepter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
this.initUserInfo((HttpServletRequest) request);
chain.doFilter(request, response);
}
private void initUserInfo(HttpServletRequest request) {
String userJson = request.getHeader("key_userinfo_in_http_header");
if (StringUtils.isNotBlank(userJson)) {
try {
userJson = URLDecoder.decode(userJson, "UTF-8");
UserInfo userInfo = JSON.parseObject(userJson, UserInfo.class);
//將UserInfo放入上下文中
UserInfoContext.setUser(userInfo);
} catch (UnsupportedEncodingException e) {
log.error("init userInfo error", e);
}
}
}
@Override
public void destroy() {
}
}
四癣漆、定義攔截器婚肆,在服務(wù)間相互調(diào)用時(shí)糟港,把訪問(wèn)用戶(hù)的信息通過(guò)請(qǐng)求頭的方式傳遞到被調(diào)用的微服務(wù)中
public class TransmitUserInfoFeighClientIntercepter implements RequestInterceptor {
private static final Logger log = LoggerFactory.getLogger(TransmitUserInfoFeighClientIntercepter.class);
@Override
public void apply(RequestTemplate requestTemplate) {
//從應(yīng)用上下文中取出user信息吭敢,放入Feign的請(qǐng)求頭中
UserInfo user = UserInfoContext.getUser();
if (user != null) {
try {
String userJson = JSON.toJSONString(user);
requestTemplate.header("KEY_USERINFO_IN_HTTP_HEADER", URLEncoder.encode(userJson, "UTF-8"));
} catch (UnsupportedEncodingException e) {
log.error("用戶(hù)信息設(shè)置錯(cuò)誤", e);
}
}
}
}