SpringSecurityOauth2实现类Oauth2并分为了两大模块,认证授权服务和资源服务。认证授权服务一般负责认证逻辑和加载用户的权限以及认证成功后为用户颁发令牌,资源服务器一般值我们提供的微服务,在资源服务器中需要对用户的令牌做检查(是否通过了认证且是否拥有访问该服务的权限)。所以我们会去创建一个认证授权的微服务,用户登录做认证和加载用户的权限。
1.导入依赖,开启启动类,配置yml文件
<dependencies>
<!--微服务基础依赖-->
<dependency>
<groupId>com.zengjx</groupId>
<artifactId>hrm-service-dependencies</artifactId>
<version>${hrm.version}</version>
</dependency>
<!--导入公共包-->
<dependency>
<groupId>com.zengjx</groupId>
<artifactId>hrm-auth-common</artifactId>
<version>${hrm.version}</version>
</dependency>
<!--导入oauth2依赖包-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!--导入alibaba的druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.20</version>
</dependency>
<!-- mysql 数据库驱动. -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--导入system的fegin的包-->
<dependency>
<groupId>com.zengjx</groupId>
<artifactId>hrm-system-feign</artifactId>
<version>${hrm.version}</version>
</dependency>
<!--导入http的依赖包-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.5</version>
</dependency>
</dependencies>
@SpringBootApplication //启动类
@EnableEurekaClient //eureka客户端
@MapperScan("com.zengjx.hrm.mapper") //这里扫描mapper层
@Import({GlobalExceptionHandler.class}) //导入该类用用来接收异常
@EnableFeignClients //开启feign
public class AuthApp {
public static void main(String[] args) {
SpringApplication.run(AuthApp.class, args);
}
}
eureka:
client:
serviceUrl:
defaultZone: http://peer1:1010/eureka/
registry-fetch-interval-seconds: 5 #拉取注册表的时间间隔
instance:
instance-id: www.auth.com:1100 #实例的ID
prefer-ip-address: true #使用IP注册到注册中心
server:
port: 1100
spring:
application:
name: service-auth
datasource:
url: jdbc:mysql:///hrm-auth
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource #指定使用阿里的连接池
#配置数据源
mybatis-plus:
mapper-locations: classpath:com/zengjx/hrm/mapper/*Mapper.xml
feign:
hystrix:
enabled: true #开启降级
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 3000
2.创建一个WebSecurityConfig去继承WebSecurityConfigurerAdapter覆写它的方法
@Configuration //声明这是个配置类
@EnableWebSecurity //开启Security的配置
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
//设置密码编码器
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//配置认证管理器
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
//配置授权规则
@Override
protected void configure(HttpSecurity http) throws Exception{
//授权配置
http.authorizeRequests() //编写授权配置
//.antMatchers("/login").permitAll() //登录路径放行
.antMatchers("/login/user").permitAll() //登录放行
.anyRequest().authenticated() //访问其他路径就都需要进行一个验证
.and().formLogin() //允许表单登录
.successForwardUrl("/loginSuccess") //登录成功后跳转的页面
.and().logout().permitAll() //登出路径放行 默认为loginOut
.and().csrf().disable(); //关闭跨域伪造检查
}
}
3.创建MyAuthorizationServerConfig类去继承AuthorizationServerConfigurerAdapter这样我们就可以去配置授权服务
@Configuration //授权服务配置
@EnableAuthorizationServer //开启Authorizatin服务
public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter{
//注入datasource
@Autowired
private DataSource dataSource;
//注入PasswordEncoder解密
@Autowired
private PasswordEncoder passwordEncoder;
//注入密码授权模式需要
@Autowired
private AuthenticationManager authenticationManager;
//1.1定义客户端详情服务,连接数据库中的oauth_client_details表中的数据
@Bean
public ClientDetailsService jdbcClientDetailsService(){
//使用JDBC的一个实现类来连接数据库
JdbcClientDetailsService jdbc = new JdbcClientDetailsService(dataSource);
//使用数据库中的秘钥来解密
jdbc.setPasswordEncoder(passwordEncoder);
//返回jdbc
return jdbc;
}
//1.将我们写的配置注入到主配置当中
public void configure(ClientDetailsServiceConfigurer clients) throws Exception{
//将我们配置信息交给主配置
clients.withClientDetails(jdbcClientDetailsService());
}
//===================================================================================
//2.1授权码的管理服务 默认回去读取oath_code表中的信息
@Bean
public AuthorizationCodeServices jdbcAuthorizationCodeServices(){
return new JdbcAuthorizationCodeServices(dataSource);
}
//2.2设置token的授权
@Bean
public AuthorizationServerTokenServices tokenService(){
//创建默认的令牌服务
DefaultTokenServices services = new DefaultTokenServices();
//指定客户端详情配置
services.setClientDetailsService(jdbcClientDetailsService());
//支持产生刷新token
services.setSupportRefreshToken(true);
//token存储方式
services.setTokenStore(tokenStore());
//设置token增强 - 设置token转换器
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter()));
services.setTokenEnhancer(tokenEnhancerChain);
return services;
}
//2.3配置token的存储方式
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
//2.4配置jwt令牌工具
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
//可以是简单的设置JWT签名密钥。它MAC密钥,也可以是RSA密钥
jwtAccessTokenConverter.setSigningKey(sign_key);
return jwtAccessTokenConverter;
}
//2.5配置临令牌转换器 设置jwt的签名密匙 可以是MAC密匙也可以是RSA密匙
private final String sign_key = "123";
//2.配置令牌和授权码
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{
endpoints
//密码授权模式
.authenticationManager(authenticationManager)
//授权码模式服务
.authorizationCodeServices(jdbcAuthorizationCodeServices())
//设置令牌管理服务
.tokenServices(tokenService())
//允许Post请求
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
//===============================================================================================
//3.授权服务安全配置
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
//对应/oauth/check_token ,路径公开 这样可以检查token
.checkTokenAccess("permitAll()")
//允许客户端进行表单身份验证,使用表单认证申请令牌
.allowFormAuthenticationForClients();
}
}
4.创建UserDetailsServiceImpl类去实现UserDetailsService这样我们就可以做登录认证和授权了
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
//注入查询登录用户的Mapper
@Autowired
private LoginInfoMapper loginInfoMapper;
//注入system用户的feign接口
@Autowired
private SystemFeignClient systemFeignClient;
//重写方法
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//使用loginInfoMapper查询到登录用户
LoginInfo loginInfo = loginInfoMapper.selectByUsername(username);
//判断用户是否存在
ValidUtils.isNotNull(loginInfo, "用户不存在");
//创建一个数据用来加载用户的权限
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
//判断是否是后台用户
if (loginInfo.getType() == LoginInfo.TYPE_SYSTEM) {
AjaxResult ajaxResult = systemFeignClient.selectUserContextInfo(loginInfo.getId());
ValidUtils.isTrue(ajaxResult.isSuccess(), "用户认证信息加载失败");
String userInfoJSON = ajaxResult.getResultObj().toString();
//接收传过来的json格式的字符串 然后解析过来的json格式的字符串转换为对象
UserContextInfo userContextInfo = JSON.parseObject(userInfoJSON,UserContextInfo.class);
//遍历权限
userContextInfo.getPermissions().forEach(permission ->{
authorities.add(new SimpleGrantedAuthority(permission));
});
//封装为UserDatils对象返回 userInfoJSON里面拥有用户的信息
return new User(userInfoJSON,
loginInfo.getPassword(),
loginInfo.getEnabled(),
loginInfo.getAccountNonExpired(),
loginInfo.getCredentialsNonExpired(),
loginInfo.getAccountNonLocked(),
authorities
);
}
return new User(loginInfo.getUsername(),
loginInfo.getPassword(),
loginInfo.getEnabled(),
loginInfo.getAccountNonExpired(),
loginInfo.getCredentialsNonExpired(),
loginInfo.getAccountNonLocked(),
authorities
);
}
}
5.在UserDetailsServiceImpl中我们需要通过feign的方式去其他的微服务中查询到登录用户的权限信息。但是,我们配置了一个微服务之间通过feign通信的时候需要携带上token的配置(为什么需要携带上token?这是因为我们在微服务哪里配置了资源服务,需要通过认证的用户才能够访问微服务提供的资源),所以问题来了,用户在做登录操作,我们正在做认证和授权操作,这时候就需要去调用用户的基本信息和权限信息(造成这个问题的原因其实是我们将权限信息放在了其他的数据库中),而被调用的微服务又需要我们提供认证信息和授权(其实就是token)。列如,你去买aj,但是你必须先穿上aj才能买到aj。
5.1微服务之间通过feign调用,让发送的请求中携带token的类(这个应该放在微服务集成公共的模块里)
@Component //交给spring管理
@Slf4j
public class FeignRequestInterceptor implements RequestInterceptor {
//定义一个请求头常量
public static final String TOKEN_NAME = "Authorization";
//这个是feign的拦截器
// feign的接口会被代理 每个方法在请求的时候会为这个方法生成一个RequestTemplate
// 封装好请求参数(url,等数据)
// 让后将RequestTemplate交给HTTP客户端去执行
// 底层用的是rebbon的负载均衡去执行的
// RequestTemplate就是feign用来发请求 封装请求的对象
@Override
public void apply(RequestTemplate requestTemplate) {
//获取请求对象 使用请求的上下文路径获取到请求中的所有数据
//这里获取到的对象需要强转为ServletRequestAttributes对象
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
if (requestAttributes != null){
javax.servlet.http.HttpServletRequest request = requestAttributes.getRequest();
//获取请求头中的token
String token = request.getHeader(TOKEN_NAME);
if (StringUtils.hasLength(token)){
//设置给requestTemplate的header
requestTemplate.header(TOKEN_NAME,token);
log.info("feign转发的token:{}",token);
}
}else {
log.error("feign拦截器,请求为空");
}
}
}
5.2由于我使用的feign是通过线程池的方式,所以还需要去修改并发策略,将请求对象放入到新的线程中(这个应该放在微服务集成公共的模块里)
//修改并发策略,把请求对象放入到新的线程中
@Configuration
public class FeignHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
private HystrixConcurrencyStrategy hystrixConcurrencyStrategy;
public FeignHystrixConcurrencyStrategy() {
try {
this.hystrixConcurrencyStrategy = HystrixPlugins.getInstance().getConcurrencyStrategy();
if (this.hystrixConcurrencyStrategy instanceof FeignHystrixConcurrencyStrategy) {
return;
}
HystrixCommandExecutionHook commandExecutionHook =
HystrixPlugins.getInstance().getCommandExecutionHook();
HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier();
HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher();
HystrixPropertiesStrategy propertiesStrategy =
HystrixPlugins.getInstance().getPropertiesStrategy();
HystrixPlugins.reset();
HystrixPlugins instance = HystrixPlugins.getInstance();
instance.registerConcurrencyStrategy(this);
instance.registerCommandExecutionHook(commandExecutionHook);
instance.registerEventNotifier(eventNotifier);
instance.registerMetricsPublisher(metricsPublisher);
instance.registerPropertiesStrategy(propertiesStrategy);
} catch (Exception e) {
System.out.println("策略注册失败");
}
}
@Override
public <T> Callable<T> wrapCallable(Callable<T> callable) {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
return new WrappedCallable<>(callable, requestAttributes);
}
@Override
public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
HystrixProperty<Integer> corePoolSize,
HystrixProperty<Integer> maximumPoolSize,
HystrixProperty<Integer> keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue) {
return this.hystrixConcurrencyStrategy.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, keepAliveTime,
unit, workQueue);
}
@Override
public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
HystrixThreadPoolProperties threadPoolProperties) {
return this.hystrixConcurrencyStrategy.getThreadPool(threadPoolKey, threadPoolProperties);
}
@Override
public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) {
return this.hystrixConcurrencyStrategy.getBlockingQueue(maxQueueSize);
}
@Override
public <T> HystrixRequestVariable<T> getRequestVariable(HystrixRequestVariableLifecycle<T> rv) {
return this.hystrixConcurrencyStrategy.getRequestVariable(rv);
}
static class WrappedCallable<T> implements Callable<T> {
private final Callable<T> target;
private final RequestAttributes requestAttributes;
public WrappedCallable(Callable<T> target, RequestAttributes requestAttributes) {
this.target = target;
this.requestAttributes = requestAttributes;
}
@Override
public T call() throws Exception {
try {
RequestContextHolder.setRequestAttributes(requestAttributes);
return target.call();
} finally {
RequestContextHolder.resetRequestAttributes();
}
}
}
}
5.3解决穿aj才能去买aj的方式,通过发送Http请求去先获取到一个临时的token,然后再携带上这个临时的token去我们需要访问的微服务拿到用户的基本信息和权限。
@Component //交给spring管理
@Slf4j
public class TempFeignRequestInterceptor implements RequestInterceptor {
//定义一个请求头常量
private static final String TOKEN_NAME = "Authorization";
//发送Url获取到临时的token
private static String TOKEN_URL = "http://localhost:1100/oauth/token?client_id=%s&client_secret=%s&grant_type=client_credentials";
//这个是feign的拦截器
// feign的接口会被代理 每个方法在请求的时候会为这个方法生成一个RequestTemplate
// 封装好请求参数(url,等数据)
// 让后将RequestTemplate交给HTTP客户端去执行
// 底层用的是rebbon的负载均衡去执行的
// RequestTemplate就是feign用来发请求 封装请求的对象
@Override
public void apply(RequestTemplate requestTemplate) {
//使用http请求获取到一个模拟的token
String sendUrl = String.format(TOKEN_URL, "temp", "123");
//发送请求获取到token
Map<String, String> accessTokenMap = HttpUtil.sendPost(sendUrl);
if (accessTokenMap != null){
//获取到token
String accessToken = accessTokenMap.get("access_token");
//将获取到token交给请求头
requestTemplate.header(TOKEN_NAME,"Bearer ",accessToken);
log.info("Feign拦截器转发token:",accessToken );
}else {
log.error("认证中心创建失败");
}
}
}
6.个人总结
6.1配置步骤有很多,它们之间是互相关联的需要深刻理解它们之间的依赖关系
6.2feign的使用和feign的拦截器。拦截feign并让它携带上token信息,但是feign使用了线程池的原因所以还需要修改并发策略,把请求对象放入到新的线程中
6.3获取临时token。通过发送一个http请求到认证中心,拿到一个临时token,然后通过feign的方式到用户信息库中获取到用户信息和用户权限信息。