您现在的位置是:首页 >技术教程 >Spring Security + oauth2一文整合网站首页技术教程
Spring Security + oauth2一文整合
简介Spring Security + oauth2一文整合
一、授权认证配置类
- CommonAuthorizationServerConfigurerAdapter继承AuthorizationServerConfigurerAdapter
- 添加认证服务注解:@EnableAuthorizationServer
- 复写三个方法
- configure(ClientDetailsServiceConfigurer clients)
- 配置oauth2的客户端相关信息
- 内存保存客户端信息
- jdbc数据库保存
-
创建EduClientDetailService数据库连接处理类
-
因为clientID每个程序都是唯一的,但是我们要针对不同系统,使用不同的clientID
- 因此需要在EduClientDetailService 中复写loadClientByClientId方法,查询出来对应的clientDetail
package com.cloud.edu.common.security.service; import com.cloud.edu.common.core.constant.SecurityConstants; import lombok.SneakyThrows; import org.springframework.cache.annotation.Cacheable; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; import javax.sql.DataSource; /** * 将客户端连接信息保存到数据库 * see JdbcClientDetailsService */ public class EduClientDetailService extends JdbcClientDetailsService { public EduClientDetailService(DataSource dataSource) { super(dataSource); } /** * 重写原生方法支持redis缓存写原生方法支持redis缓存 * * @param clientId * @return */ @Override @SneakyThrows @Cacheable(value = SecurityConstants.CLIENT_DETAILS_KEY, key = "#clientId", unless = "#result == null") public ClientDetails loadClientByClientId(String clientId) { return super.loadClientByClientId(clientId); } }
- 生成固定的数据库,执行oauth_client_details.sql文件即可
DROP TABLE IF EXISTS `oauth_client_details`; CREATE TABLE `oauth_client_details` ( `client_id` varchar(48) NOT NULL, `resource_ids` varchar(256) DEFAULT NULL, `client_secret` varchar(256) DEFAULT NULL, `scope` varchar(256) DEFAULT NULL, `authorized_grant_types` varchar(256) DEFAULT NULL, `web_server_redirect_uri` varchar(256) DEFAULT NULL, `authorities` varchar(256) DEFAULT NULL, `access_token_validity` int(11) DEFAULT NULL, `refresh_token_validity` int(11) DEFAULT NULL, `additional_information` varchar(4096) DEFAULT NULL, `autoapprove` varchar(256) DEFAULT NULL, PRIMARY KEY (`client_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-
在数据库中根据自身业务添加对应的数据信息
- 例如:后台管理端登录,比较简易的
- authorized_grant_types:password,refresh_token
- scope:all
- client_id:manager
- client_secret:123456
- autoapprove:true
- 这些数据authorized_grant_types是要根据认证类型来的
- 例如:后台管理端登录,比较简易的
-
- 配置oauth2的客户端相关信息
- configure(AuthorizationServerSecurityConfigurer security)
- 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)安全策略
- configure(AuthorizationServerEndpointsConfigurer endpoints)
- 配置令牌端点(Token Endpoint)的安全约束端点
- 配置token的存储方式
-
InMemoryTokenStore(默认)
- 内存存储模型,并发要求较低模式下可以选择,无需任何配置,使用默认的tokenStore注入即可
@Autowired private TokenStore InMemoryTokenStore; endpoints.tokenStore(InMemoryTokenStore);
-
JdbcTokenStore
- 自定义jdbcToken即可
@Bean public TokenStore jdbcTokenStore(DataSource dataSource) { return new JdbcTokenStore(dataSource); } @Autowired private TokenStore jdbcTokenStore; endpoints.tokenStore(jdbcTokenStore);
-
JwtTokenStore
- JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的
- 配置JwtTokenStore ,并向token添加jwtSigningKey和自定义的相关信息
@Bean public JwtTokenStore jwtTokenStore() { return new JwtTokenStore(accessTokenConverter()); } @Bean public JwtAccessTokenConverter accessTokenConverter() { CommonJwtTokenEnhancer converter = new CommonJwtTokenEnhancer(); converter.setSigningKey(“jwtSigningKey”); return converter; } @Autowired private TokenStore jwtTokenStore; @Autowired private JwtAccessTokenConverter jwtAccessTokenConverter; endpoints.tokenStore(jwtTokenStore).accessTokenConverter(jwtAccessTokenConverter);
-
RedisTokenStore
private final RedisConnectionFactory redisConnectionFactory; @Bean public TokenStore tokenStore() { RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory); tokenStore.setPrefix(SecurityConstants.PROJECT_PREFIX + SecurityConstants.OAUTH_PREFIX); return tokenStore; }
-
- configure(ClientDetailsServiceConfigurer clients)
二、资源授权配置类
- CommonResourceServerAdapter继承ResourceServerConfigurerAdapter
package com.zcgk.xmdj.web.config;
import com.zcgk.xmdj.service.constants.SystemConstants;
import com.zcgk.xmdj.web.handler.CommonAccessDeniedHandler;
import com.zcgk.xmdj.web.handler.CommonAuthenticationEntryPointHandler;
import com.zcgk.xmdj.web.jwt.CommonJwtAuthenticationFilter;
import com.zcgk.xmdj.web.security.CommonAccessDecisionManager;
import com.zcgk.xmdj.web.security.CommonSecurityMetadataSourceManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import java.util.Arrays;
import java.util.List;
/**
* 资源权限认证类
*/
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled=true, securedEnabled = true) //使用表达式时间方法级别的安全性 && 过滤权限
public class CommonResourceServerAdapter extends ResourceServerConfigurerAdapter {
//资源认证在一个服务器
@Autowired private DefaultTokenServices tokenServices;
//资源认证在两个服务器,需要remoteTokenServices远程认证token获取用户信息
@Autowired private RemoteTokenServices remoteTokenServices;
@Autowired private JwtTokenStore tokenStore;
//用来根据URL动态获取资源权限
@Autowired private CommonSecurityMetadataSourceManager securityMetadataSourceManager;
//资源认证管理-用来认证资源是否允许访问
@Autowired private CommonAccessDecisionManager accessDecisionManager;
@Autowired private CommonAuthenticationEntryPointHandler commonAuthenticationEntryPointHandler;
@Autowired private CommonAccessDeniedHandler commonAccessDeniedHandler;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
/**
* 如果授权和认证不在一个服务器,可以使用remoteTokenServices进行远程认证
* 在授权服务器调用认证服务器获取token并获取用户信息
* remoteTokenServices 最终会主席那个loadAuthentication方法
* return this.tokenConverter.extractAuthentication(map);
* tokenConverter就是注入的token增强器
*
* 1、我们使用DefaultAccessTokenConverter 并且添加上自定义的UserTokenConverter
* 1.1 remoteTokenServices 会执行DefaultAccessTokenConverter 类的extractAuthentication方法
* 1.2 extractAuthentication方法中会执行 extractAuthentication方法
* 1.2.1 EduUserAuthenticationConverter实现UserAuthenticationConverter 并重写 extractAuthentication方法,实现自身逻辑
* -- extractAuthentication方法 的参数 Map<String, ?> map 就是我们的 Authentication对象的Map表现形式
* 包括:自定义的用户信息(通常有用户名、编号等自己添加到token中的信息以及 authorities权限信息)
*
*/
// DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
// UserAuthenticationConverter userTokenConverter = new EduUserAuthenticationConverter();
// accessTokenConverter.setUserTokenConverter(userTokenConverter);
// //认证地址 auth 是认证微服务的名字 /oauth/check_token 是固定的接口api
// remoteTokenServices.setCheckTokenEndpointUrl("http://auth/oauth/check_token");
// remoteTokenServices.setAccessTokenConverter(accessTokenConverter);
// remoteTokenServices.setClientId("client-id");
// remoteTokenServices.setClientSecret("client-secret");
resources.accessDeniedHandler(commonAccessDeniedHandler)
.tokenServices(tokenServices)
// .tokenServices(remoteTokenServices)
.authenticationEntryPoint(commonAuthenticationEntryPointHandler);
}
@Override
public void configure(HttpSecurity http) throws Exception {
List<String> permitAllEndpointList = Arrays.asList(
SystemConstants.API_TOKEN_URL, SystemConstants.API_USER_INFO
);
CommonJwtAuthenticationFilter jwtFilter = new CommonJwtAuthenticationFilter(tokenStore); //添加token解析拦截器,贯穿用户信息到线程上下文
http.authorizeRequests()
.antMatchers(permitAllEndpointList.toArray(new String[permitAllEndpointList.size()])).permitAll()//不需要权限的接口
.anyRequest().authenticated() //所有请求都需要权限
//自定义配置FilterSecurityInterceptor中的对象信息
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(accessDecisionManager);
object.setSecurityMetadataSource(securityMetadataSourceManager);
return object;
}
})
.and().addFilterBefore(jwtFilter, FilterSecurityInterceptor.class);
}
}
- 添加授权服务注解:@EnableResourceServer
- 复写两个方法
- configure(ResourceServerSecurityConfigurer resources)
- 主要用于配置认证授权处理类
- configure(HttpSecurity http)
- 主要增加一些资源认证过滤器、免权限接口等
- configure(ResourceServerSecurityConfigurer resources)
- 如果资源和认证不在一个服务器,需要使用remoteTokenServices进行认证远程调用
- 在认证服务器端设置对应的Client信息
- 认证服务器默认有一个/oauth/check_token的认证access_token的接口
- 具体流程
- remoteTokenServices类中会执行loadAuthentication方法
- loadAuthentication方法中执行了认证的远程调用
- 并将返回的用户信息的map对象,调用DefaultAccessTokenConverter的extractAuthentication方法
- extractAuthentication方法中 this.userTokenConverter.extractAuthentication(map)
- userTokenConverter就是注入的UserAuthenticationConverter
- 我们自定义MyUserAuthenticationConverter类继承UserAuthenticationConverter
- 复写extractAuthentication方法,方法的参数map就是我们在客户端定义的增强对象
- map中包含系统内置的authorities、user_name以及自定义用户的属性
- 复写extractAuthentication方法,方法的参数map就是我们在客户端定义的增强对象
- 我们自定义MyUserAuthenticationConverter类继承UserAuthenticationConverter
- userTokenConverter就是注入的UserAuthenticationConverter
- remoteTokenServices类中会执行loadAuthentication方法
package com.cloud.edu.common.security.component;
import com.cloud.edu.common.security.service.EduUser;
import com.cloud.edu.common.security.service.EduUserDetailServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* <p>
* 根据checktoken 的结果转化用户信息
*/
public class MyUserAuthenticationConverter implements UserAuthenticationConverter {
private static final String USER_ID = "userId";
private static final String Name = "name";
private static final String User_No = "userNo";
private static final String N_A = "N/A";
/**
* Extract information about the user to be used in an access token (i.e. for resource servers).
*
* @param authentication an authentication representing a user
* @return a map of key values representing the unique information about the user
*/
@Override
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
Map<String, Object> response = new LinkedHashMap<>();
response.put(USERNAME, authentication.getName());
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return response;
}
/**
* Inverse of {@link #convertUserAuthentication(Authentication)}. Extracts an Authentication from a map.
*
* @param map a map of user information
* @return an Authentication representing the user or null if there is none
*/
@Override
public Authentication extractAuthentication(Map<String, ?> map) {
if (map.containsKey(USERNAME)) {
Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
String username = (String) map.get(USERNAME);
Integer id = (Integer) map.get(USER_ID);
Integer userNo = (Integer) map.get(User_No);
EduUser user = new EduUser(id, userNo, username, N_A, true
, true, true, true, authorities);
return new UsernamePasswordAuthenticationToken(user, N_A, authorities);
}
return null;
}
@Autowired
private UserDetailsService userDetailsService;
private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
Object authorities = map.get(AUTHORITIES);
if (authorities instanceof String) {
return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
}
if (authorities instanceof Collection) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils
.collectionToCommaDelimitedString((Collection<?>) authorities));
}
if (authorities == null) {
if (this.userDetailsService != null) {
UserDetails user = this.userDetailsService.loadUserByUsername((String) map.get("user_name"));
return user.getAuthorities();
}
}
throw new IllegalArgumentException("Authorities must be either a String or a Collection");
}
}
三、token增强器
- CommonJwtTokenEnhancer继承JwtAccessTokenConverter
- 复写三个方法
- enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication)
- 主要方法:添加自定义信息(用户信息)到token中
- extractAccessToken(String value, Map<String, ?> map)
- extractAuthentication(Map<String, ?> claims)
- 添加用户信息到OAuth2Authentication对象
- enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication)
package com.zcgk.xmdj.web.jwt;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import java.util.HashMap;
import java.util.Map;
/**
* jwt增强器,将jwt转化为token
*/
public class CommonJwtTokenEnhancer extends JwtAccessTokenConverter {
/**
* 添加自定义信息到token中
* @param accessToken
* @param authentication
* @return
*/
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, Object> tokenExtra = new HashMap<>();
CommonJwtUser user = (CommonJwtUser) authentication.getUserAuthentication().getPrincipal();
tokenExtra.put("userNo", user.getUserNo());
tokenExtra.put("name", user.getName());
tokenExtra.put("userId", user.getUserId());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(tokenExtra);
accessToken = super.enhance(accessToken, authentication);
return accessToken;
}
/**
* 解析token
* @param value
* @param map
* @return
*/
@Override
public OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map) {
OAuth2AccessToken oauth2AccessToken = super.extractAccessToken(value, map);
Map<String, Object> additionInfoMap = oauth2AccessToken.getAdditionalInformation();
return oauth2AccessToken;
}
/**
* 设置用户信息到对象信息
* @param claims
* @return
*/
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> claims) {
OAuth2Authentication authentication = super.extractAuthentication(claims);
authentication.setDetails(claims);
return authentication;
}
}
四、token解析拦截器
- CommonJwtAuthenticationFilter继承OncePerRequestFilter
- OncePerRequestFilter:通过request.getAttribute判断当前过滤器是否已执行,保证只会被执行一次
- 复写doFilterInternal方法
- 通过request获取用户对象:Authentication authentication = tokenExtractor.extract(request)
- 通过用户对象获取token:String tokenValue = authentication.getPrincipal().toString();
- 读取用户信息:OAuth2Authentication oauth = tokenStore.readAuthentication(tokenValue);
- 用户信息:Map<String, String> userInfoMap = (Map<String, String>) oauth.getDetails();
package com.zcgk.xmdj.web.jwt;
import com.zcgk.xmdj.service.constants.ContextUserDto;
import com.zcgk.xmdj.service.constants.ContextUserLocal;
import com.zcgk.xmdj.service.constants.SystemConstants;
import com.zcgk.xmdj.utils.Checker;
import com.zcgk.xmdj.web.exception.ClientException;
import com.zcgk.xmdj.web.exception.ClientExceptionEnum;
import lombok.Setter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.authentication.BearerTokenExtractor;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.web.filter.OncePerRequestFilter;
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.Collection;
import java.util.Map;
/**
* 拦截器会按照链式顺序依次执行doFilter()方法
* 只拦截一次过滤,获取登陆的用户信息,并放入上下文中
* OncePerRequestFilter.doFilter方法中通过request.getAttribute判断当前过滤器是否已执行
* 若未执行过,则调用doFilterInternal方法,交由其子类实现
*/
public class CommonJwtAuthenticationFilter extends OncePerRequestFilter {
@Setter
private JwtTokenStore tokenStore;
//分离出请求中包含的token
private final BearerTokenExtractor tokenExtractor = new BearerTokenExtractor();
public CommonJwtAuthenticationFilter(JwtTokenStore tokenStore) {
this.tokenStore = tokenStore;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
//判断是否是跨域请求
if(SystemConstants.METHOD_OPTION.equals(request.getMethod())) {
return;
}
//提取token
Authentication authentication = tokenExtractor.extract(request);
if(Checker.BeNull(authentication)) {
throw new ClientException(ClientExceptionEnum.INVALID_TOKEN);
}
String tokenValue = authentication.getPrincipal().toString();
OAuth2Authentication oauth = tokenStore.readAuthentication(tokenValue);//读取用户信息
if (Checker.BeNull(oauth)) {
throw new ClientException(ClientExceptionEnum.INVALID_TOKEN);
}
Map<String, String> userInfoMap = (Map<String, String>) oauth.getDetails();
if(Checker.BeNull(userInfoMap.get(SystemConstants.USER_ID))) {
throw new ClientException(ClientExceptionEnum.INVALID_TOKEN);
}
// 设置用户信息到上下文
ContextUserDto user = new ContextUserDto();
user.setUserId(userInfoMap.get(SystemConstants.USER_ID))
.setName(userInfoMap.get(SystemConstants.USER_NAME));
ContextUserLocal.put(user);
//分布式环境下放到session中,seesion保存到redis中
request.getSession().setAttribute("userinfo", user);
try {
//他的作用是将请求转发给过滤器链上下一个filter,如果没有filter那就是你请求的资源。
filterChain.doFilter(request, response);
} finally {
ContextUserLocal.remove();
}
}
}
五、登陆认证成功处理类(security)
- MyAuthenticationSuccessHandler继承SavedRequestAwareAuthenticationSuccessHandler
- 在WebSecurityConfig中可以绑定successHandler()
- 复写onAuthentication
- 可以获取到当前登录的Authentication对象,根据相关信息进行路由跳转或者其他响应信息
- 可以返回菜单,前端拿到菜单直接跳转到对应的菜单
- 可以返回完整的地址,或者第三方地址等
- 原始的mvc模式可以直接redirict重定向页面
package com.zcgk.xmdj.web.security;
import com.alibaba.fastjson.JSON;
import com.zcgk.xmdj.web.common.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 登录成功处理类
* @author zzx
* @version 1.0.0
*/
@Slf4j
@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("登录成功");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(ResponseResult.result()));
}
}
六、登陆认证失败处理类(security)
- MyAuthenticationFailHandler继承SimpleUrlAuthenticationFailureHandler
- 在WebSecurityConfig中可以绑定failerHandler()
- 复写onAuthenticationFailure,可以获取到认证失败的Exception
- 可以返回对应的错误信息或者跳转连接
package com.zcgk.xmdj.web.security;
import com.alibaba.fastjson.JSON;
import com.zcgk.xmdj.web.common.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 登陆失败的异常处理类
* @author zzx
* @version 1.0.0
*/
@Slf4j
@Component
public class AuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
log.info("登录失败");
//将 登录失败 信息打包成json格式返回
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(ResponseResult.result()));
}
}
七、登录结果事件监听(security)
- 处理两个登录的处理类,security还提供了两个登录的事件
- AuthenticationSuccessEvent和AbstractAuthenticationFailureEvent
- 例如: AuthenticationFailureEvenHandler implements ApplicationListener
- 创建一个事件监听处理类,实现监听器,绑定监听事件即可
- 可以获取认证后的对象:(Authentication) authenticationSuccessEvent.getSource();
- 可以获取认证后的异常:AuthenticationException authenticationException = event.getException();
- AuthenticationSuccessEvent和AbstractAuthenticationFailureEvent
八、已认证但无权限处理类(security)
- CommonAccessDeniedHandler实现AccessDeniedHandler(OAuth2AccessDeniedHandler)
- 返回暂无权限状态码:403
- 抛出暂无资源权限
package com.zcgk.xmdj.web.security;
import cn.hutool.http.HttpStatus;
import com.alibaba.fastjson.JSONObject;
import com.cloud.edu.common.core.constant.CommonConstants;
import com.zcgk.xmdj.utils.Checker;
import com.zcgk.xmdj.web.common.ResponseResult;
import com.zcgk.xmdj.web.exception.ClientException;
import com.zcgk.xmdj.web.exception.ClientExceptionEnum;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 已验证但无权限处理类
*/
@Component
public class CommonAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
if(Checker.BeNull(response)) {
throw new ClientException("response can't be null");
}
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpStatus.HTTP_FORBIDDEN);
response.getWriter().write(JSONObject.toJSONString(accessDeniedException.getMessage()));
ResponseResult.result(ClientExceptionEnum.RESOURCE_VERIFY_FAIL.getCode(), JSONObject.toJSONString(accessDeniedException.getMessage()));
throw new ClientException(ClientExceptionEnum.RESOURCE_VERIFY_FAIL);
}
}
九、未认证的无权限处理类(security)
- CommonAuthenticationEntryPointHandler实现AuthenticationEntryPoint
- 返回认证失败状态码:401
- 抛出认证失败,token无效、
package com.zcgk.xmdj.web.security;
import com.alibaba.fastjson.JSONObject;
import com.zcgk.xmdj.utils.Checker;
import com.zcgk.xmdj.web.common.ResponseResult;
import com.zcgk.xmdj.web.exception.ClientException;
import com.zcgk.xmdj.web.exception.ClientExceptionEnum;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 未认证的无权限处理类
*/
@Component
public class CommonAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
if(Checker.BeNull(response)) {
throw new ClientException("response can't be null");
}
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write(JSONObject.toJSONString(authException.getMessage()));
ResponseResult.result(ClientExceptionEnum.INVALID_TOKEN.getCode(), JSONObject.toJSONString(authException.getMessage()));
throw new ClientException(ClientExceptionEnum.INVALID_TOKEN);
}
}
十、跨域处理器
- 端口、协议、ip任意一点不同都会存在跨域问题
package com.zcgk.xmdj.web.config;
import com.google.common.collect.ImmutableList;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import reactor.core.publisher.Mono;
import javax.servlet.ServletException;
/**
* 跨域请求过滤器
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CommonCorsFilter extends CorsFilter {
public CommonCorsFilter() {
super(configurationSource());
}
private static UrlBasedCorsConfigurationSource configurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(ImmutableList.of("*"));
config.setAllowedHeaders(ImmutableList.of("*"));
config.setAllowedMethods(ImmutableList.of("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
十一、Feign进行远程调用,请求头token的问题
- 当我们带着请求头token去调用A服务的时候,可能会存在服务与服务之间的远程调用
- 调用B服务(也会进行token的校验),因此需要使用feign传递token
- oauth2提供了一份feign的拦截器:OAuth2FeignRequestInterceptor
- 拦截器调用apply方法从oAuth2ClientContext中获取token,
- AccessTokenContextRelay类通过copyToken方法从上下文张获取Authentication对象,并解析出来token放入上下文中
- 所面临的问题
- 如果一个feign请求不需要token或者还没有token生成的阶段,如果copyToken,则会造成错误
- 解决方案:重写apply方法,判断请求的类型
- apply方法的参数RequestTemplate,就是将要构建的请求对象模板
- 不需要token的可以在请求头添加自定义的信息,例如以下:
- 可以从模板的请求头中获取from的标志对象头
- Collection fromHeader = template.headers().get(“from”);
- 如果from请求头中包含的是Y,则代表是内部调用接口,也就是不需要token的接口,直接return,无需copytoken
- 可以从模板的请求头中获取from的标志对象头
- 具体代码
- 创建配置类EduFeignClientInterceptor继承OAuth2FeignRequestInterceptor,复写apply方法
package com.cloud.edu.common.security.feign;
import cn.hutool.core.collection.CollUtil;
import com.cloud.edu.common.core.constant.SecurityConstants;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.security.oauth2.client.AccessTokenContextRelay;
import org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import java.util.Collection;
/**
* 扩展OAuth2FeignRequestInterceptor
*/
@Slf4j
public class EduFeignClientInterceptor extends OAuth2FeignRequestInterceptor {
private final OAuth2ClientContext oAuth2ClientContext;
private final AccessTokenContextRelay accessTokenContextRelay;
/**
* Default constructor which uses the provided OAuth2ClientContext and Bearer tokens
* within Authorization header
*
* @param oAuth2ClientContext provided context
* @param resource type of resource to be accessed
* @param accessTokenContextRelay
*/
public EduFeignClientInterceptor(OAuth2ClientContext oAuth2ClientContext
, OAuth2ProtectedResourceDetails resource, AccessTokenContextRelay accessTokenContextRelay) {
super(oAuth2ClientContext, resource);
this.oAuth2ClientContext = oAuth2ClientContext;
this.accessTokenContextRelay = accessTokenContextRelay;
}
/**
* Create a template with the header of provided name and extracted extract
* 1. 如果使用 非web 请求,header 区别
* 2. 根据authentication 还原请求token
*
* @param template
*/
@Override
public void apply(RequestTemplate template) {
Collection<String> fromHeader = template.headers().get(SecurityConstants.FROM);
if (CollUtil.isNotEmpty(fromHeader) && fromHeader.contains(SecurityConstants.FROM_IN)) {
return;
}
accessTokenContextRelay.copyToken();
if (oAuth2ClientContext != null && oAuth2ClientContext.getAccessToken() != null) {
super.apply(template);
}
}
}
package com.cloud.edu.common.security.feign;
import feign.RequestInterceptor;
import lombok.AllArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.security.oauth2.client.AccessTokenContextRelay;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
/**
* feign 拦截器传递 header 中oauth token,
* 使用hystrix 的信号量模式 hytrix隔离策略是thread,无法读到 threadLocal变量
*/
@Configuration
@AllArgsConstructor
@ConditionalOnProperty("security.oauth2.client.client-id")
public class EduFeignClientConfiguration {
@Bean
public RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext,
OAuth2ProtectedResourceDetails resource,
AccessTokenContextRelay accessTokenContextRelay) {
return new EduFeignClientInterceptor(oAuth2ClientContext, resource, accessTokenContextRelay);
}
}
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。