前后端分离的微服务间安全通讯:
之前的认证方式存在三个问题:
1.效率低,每次认证都需要去认证服务器去调用认证服务
2.不安全,传递用户身份是通过在请求头中通过明文传递
3.服务间传递信息比较麻烦,需要在请求头中存储用户信息才能处理
以上问题解决方案就需要使用JWT。
JWT(Java Web Token):
code:
在auth服务器中修改
/**
* @author aric
* @create 2021-04-06-10:54
* @fun 认证服务器配置
*/
@Configuration
@EnableAuthorizationServer //作为授权服务器存在注解
public class OAuth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private PasswordEncoder passwordEncoder;
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(jwtTokenEnhancer());
}
@Bean
public JwtAccessTokenConverter jwtTokenEnhancer() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//converter.setSigningKey("123456"); 对数据进行签名,注意不要泄漏,不然会别人签的也会认,一般会用证书进行签名,用一下替换
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("xuyu.key") /*证书的名字*/, "123456".toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("xuyu")); //作为加密的私钥
return converter;
}
//让认证服务器知道哪些应用回来找它要令牌
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//先注册到内存,后面到数据库操作
clients.inMemory()
.withClient("orderApp")
.secret(passwordEncoder.encode("123456")) //应用的名字和密码
.scopes("read","write") //访问资源服务器 -> 做权限控制
.accessTokenValiditySeconds(3600) //令牌有效期
.resourceIds("order-server") //可以访问的资源服务器
.authorizedGrantTypes("password") //授权方式
}
//让认证服务器知道哪些人回来访问
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints){
endpoints
.tokenStore(tokenStore())
.tokenEnhancer(jwtTokenEnhancer())
.authenticationManager(authenticationManager); //托管给authenticationManager做校验用户信息是否合法
}
//订单服务(谁)找认证服务器验证
@Override
public void configure(AuthorizationServerSecurityConfigurer security){
security
.tokenKeyAccess("isAuthenticated()") //oAuth往外暴露一个服务,只有经过认证的请求才能拿到tokenKey,去验证签名
.checkTokenAccess("isAuthenticated()");
}
}
修改网关相关配置:
pom.xml添加:
#配置获取验证jwt签名的key的地址,就可以把auth服务器中的密钥返回回来
security:
oauth2:
# 访问这个url必须身份认证
client:
client-id: gateway
client-sercret: 123465
# 获取密钥的地址
resource:
jwt:
key-uri: http://auth.xuyu.com:9090/oauth/oauth/token_key
配置类直接用一下替换:
/**
* @author aric
* @create 2021-05-24-10:46
* @fun
*/
@Configuration
@EnableResourceServer
public class GatewaySecurityConfig extends ResourceServerConfigurerAdapter{
/*配置表达式处理器*/
@Autowired
private GatewayWebSecurityExpressionHandler gatewayWebSecurityExpressionHandler;
@Override
public void configure(ResourceServerSecurityConfigurer resources){
resources.expressionHandler(gatewayWebSecurityExpressionHandler);
}
public void configure(HttpSecurity http){
http.authorizeRequests()
.antMatchers("/token/**").permitAll() //申请令牌的请求不需要携带令牌
// .anyRequest().authenticated(); //默认禁用所有请求都需要身份认证
.anyRequest().access("#permissionService.hasPermission(request,authentication)") //指定访问规则
}
}
使用表达式处理器
/**
* @author aric
* @create 2021-05-24-14:20
* @fun
*/
@Service
public class PermissionServiceImpl implements PermissionService {
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
System.out.println(request.getRequestURI());
System.out.println(ReflectionToStringBuilder.toString(authentication));
//有50%几率有权限访问服务
return RandomUtils.nextInt() % 2 == 0;
}
}
表达式处理器实现:
/**
* @author aric
* @create 2021-05-24-14:24
* @fun 表达式处理器
*/
@Component
public class GatewayWebSecurityExpressionHandler extends OAuth2WebSecurityExpressionHandler {
@Autowired
private PermissionServiceImpl permissionService;
@Override
protected StandardEvaluationContext createEvaluationContextInternal(Authentication authentication,FilterInvocation invocation){
StandardEvaluationContext sec = super.createEvaluationContextInternal(authentication,invocation);
sec.setVariable("permissionService",permissionService);
return sec;
}
}
订单服务修改:
pom.xml添加:
#配置获取验证jwt签名的key的地址,就可以把auth服务器中的密钥返回回来
security:
oauth2:
# 访问这个url必须身份认证
client:
client-id: orderService
client-sercret: 123465
# 获取密钥的地址
resource:
jwt:
key-uri: http://auth.xuyu.com:9090/oauth/oauth/token_key
启动类添加注解:
@Configuration
@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
@EnableResourceServer
@EnableGloabalMethodSecurity(prePostEnable = true)
public class OrderApi {
@Bean
public OAuth2RestTemplate oAuth2RestTemplate(OAuth2ProtectedResourceDetails resource, OAuth2ClientContext context){
return new OAuth2RestTemplate(resource, context);
}
public static void main(String[] args) {
SpringApplication.run(OrderApi.class,args);
}
}
UserDetailsSerImpl配置类:
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = new User();
user.setUsername(username);
user.setId(1L);
return user;
}
private PasswordEncoder passwordEncoder;
public UserDetails loadUserByUsername2(String username){
return User.withUsername(username)
.password(passwordEncoder.encode("123456"))
.authorities("ROLE_ADMIN")
.build();
}
}
登录接口:
@RestController
@RequestMapping("/orders")
@Slf4j
public class OrderController {
@Autowired
private OAuth2RestTemplate oauthRestTemplate;
//远程调用
@PostMapping
//ACL方式根据oauth的Scope权限认证,须在启动类上添加@EnableGloabalMethodSecurity(prePostEnable = true)
@PreAuthorize("#oauth2.hasScope('write')")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public OrderInfo create(@RequestBody OrderInfo info){
PriceInfo price = oauthRestTemplate.getForObject("http://localhost:9060/prices/" + info.getProductId(), PriceInfo.class);
log.info("price is "+ price.getPrice());
return info;
}
//带有jwt令牌的的处理逻辑
@PostMapping
public OrderInfo create(@RequestBody OrderInfo info, @AuthenticationPrincipal String username){
log.info("user is "+ username);
return info;
}
}
订单服务调用价格服务:
配置文件:
server:
port: 9060
security:
oauth2:
client:
client-id: priceService
client-secret: 123456
resource:
jwt:
key-uri: http://auth.xuyu.com:9090/oauth/token_key
启动类添加:
@EnableResourceServer
Controller:
@RestController
@RequestMapping("/prices")
@Slf4j
public class PriceController {
@GetMapping("/{id}")
public PriceInfo getPrice(@PathVariable Long id, @AuthenticationPrincipal String username){
System.out.println("productId is"+ id);
System.out.println("user is"+ username);
}
}
最后微服务模型:
网关添加日志记录:
日志过滤:
public class GatewayAuditLogFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String user = (String)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
System.out.println("1. add log for" +user);
filterChain.doFilter(request,response);
if(StringUtils.isNotBlank((String) request.getAttribute("logUpdated"))) {
System.out.println("3. update log to success");
}
}
}
配置信息加载:
@Configuration
@EnableResourceServer
public class GatewaySecurityConfig extends ResourceServerConfigurerAdapter{
/*配置表达式处理器*/
@Autowired
private GatewayWebSecurityExpressionHandler gatewayWebSecurityExpressionHandler;
/*自己实现的错误处理方式*/
@Autowired
private GatewayAccessDeniedHandler accessDeniedHandler;
@Override
public void configure(ResourceServerSecurityConfigurer resources){
resources
.accessDeniedHandler(accessDeniedHandler) //处理异常信息
.expressionHandler(gatewayWebSecurityExpressionHandler);
}
@Override
public void configure(HttpSecurity http){
http.authorizeRequests()
.addFilterBefore(new GatewayAuditLogFilter(),ExceptionTranslationFilter.class)
.antMatchers("/token/**").permitAll() //申请令牌的请求不需要携带令牌
// .anyRequest().authenticated(); //默认禁用所有请求都需要身份认证
.anyRequest().access("#permissionService.hasPermission(request,authentication)"); //使用给自己配置的表达式处理器
}
}
日志实现:
@Component
public class GatewayAccessDeniedHandler extends OAuth2AccessDeniedHandler{
@Override
public void handle(HttpServlet request, HttpServletResponse response, AccessDeniedException authException){
System.out.println("2. update log 403");
request.setAttribute("logUpdated","yes");
super.handle(request,response,authException);
}
}
认证错误处理:
@Component
public class GatewayAuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,AuthenticationException authException){
if(authException instanceof AccessToeknRequiredException){
//没有传入令牌
System.out.println("2. update log to 401");
} else {
//令牌有问题
System.out.println("2. add log 401");
}
request.setAttribute("logUpdated","yes");
super.commence(request, response, authException);
}
}
权限没有认证错误:
@Service
public class PermissionServiceImpl implements PermissionService {
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
System.out.println(request.getRequestURI());
System.out.println(ReflectionToStringBuilder.toString(authentication));
if(authentication instanceof AnonymousAuthenticationToken){
throw new AccessTokenRequiredException(null);
}
//有50%几率有权限访问服务
return RandomUtils.nextInt() % 2 == 0;
}
}
网关添加限流:
/**
* @author aric
* @create 2021-05-24-17:06
* @fun 限流过滤器
*/
public class GatewayRateLimitFilter extends OncePerRequestFilter {
private RateLimiter rateLimiter = RateLimiter.create(1);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("0 rate limit");
if(rateLimiter.tryAcquire()){
//获取到权限
filterChain.doFilter(request,response);
}else{
//获取不到权限
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"too many request\"}");
response.getWriter().flush();
return;
}
}
}
将限流加入Config:
@Configuration
@EnableResourceServer
public class GatewaySecurityConfig extends ResourceServerConfigurerAdapter{
/*配置表达式处理器*/
@Autowired
private GatewayWebSecurityExpressionHandler gatewayWebSecurityExpressionHandler;
/*自己实现的错误处理方式*/
@Autowired
private GatewayAccessDeniedHandler accessDeniedHandler;
@Autowired
private GatewayAuthenticationEntryPoint authenticationEntryPoint;
@Override
public void configure(ResourceServerSecurityConfigurer resources){
resources
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler) //处理异常信息
.expressionHandler(gatewayWebSecurityExpressionHandler);
}
//过滤器链
@Override
public void configure(HttpSecurity http){
http
.addFilterBefore(new GatewayRateLimitFilter(),SecurityContextPersistenceFilter.class) //限流
.addFilterBefore(new GatewayAuditLogFilter(),ExceptionTranslationFilter.class) //日志
.authorizeRequests()
.antMatchers("/token/**").permitAll() //申请令牌的请求不需要携带令牌
// .anyRequest().authenticated(); //默认禁用所有请求都需要身份认证
.anyRequest().access("#permissionService.hasPermission(request,authentication)"); //使用给自己配置的表达式处理器
}
}
总结:
jwt改造后网关所经历的顺序: