在微服务项目中,需要对整个微服务系统进行权限校验,通常有两种方案,其一是每个微服务各自鉴权,其二是在网关统一鉴权,第二种方案只需要一次鉴权就行,避免了每个微服务重复鉴权的麻烦,本文以网关统一鉴权为例介绍如何搭建微服务鉴权项目。
本文案例中共有四个微服务模块,服务注册中心、网关服务、鉴权服务和业务提供者
案例中使用组件版本号如下:
组件 | 版本 |
---|---|
JDK | 11 |
SpringBoot | 2.7.9 |
SpringCloud | 2021.0.6 |
Mybatis-Plus | 3.5.3.1 |
jjwt | 0.11.5 |
新建一个SpringBoot项目,命名为springcloud-auth-server
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>${mybatis-plus.version}version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-sleuthartifactId>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-apiartifactId>
<version>${jjwt.version}version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-implartifactId>
<version>${jjwt.version}version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-jacksonartifactId>
<version>${jjwt.version}version>
dependency>
<dependency>
<groupId>com.mysqlgroupId>
<artifactId>mysql-connector-jartifactId>
<scope>runtimescope>
dependency>
@Service
public class JwtService {
private static final String SECRET = "JOE38R39GNGRTU49Y534YNIGEYR534YNDEUR7964GEUR735";
public void validateToken(final String token) {
Jwts.parserBuilder()
.setSigningKey(getSignKey())
.build()
.parseClaimsJws(token);
}
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, username);
}
private String createToken(Map<String, Object> claims, String username) {
return Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setIssuedAt(new Date(Instant.now().toEpochMilli()))
.setExpiration(new Date(Instant.now().toEpochMilli() + 1000 * 30 * 60))
.signWith(getSignKey(), SignatureAlgorithm.HS256)
.compact();
}
private Key getSignKey() {
byte[] keyBytes = Decoders.BASE64.decode(SECRET);
return Keys.hmacShaKeyFor(keyBytes);
}
}
@Configuration
@EnableWebSecurity
public class AuthConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.authorizeHttpRequests()
.antMatchers("/auth/register", "/auth/token", "/auth/validate").permitAll();
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
return new CustomUserDetailsService();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService());
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
}
新建CustomUserDetails类实现UserDetails接口
public class CustomUserDetails implements UserDetails {
private String username;
private String password;
public CustomUserDetails(UserCredential userCredential) {
this.username = userCredential.getUsername();
this.password = userCredential.getPassword();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
编写CustomUserDetailsService类实现UserDetailsService接口
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private AuthService authService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<UserCredential> credential = authService.findUserByUsername(username);
return credential.map(CustomUserDetails::new).orElseThrow(() -> new UsernameNotFoundException("user not found"));
}
}
说明:文中用到的用户认证类UserCredential只有用户名和密码字段,实体类、持久层接口和业务类接口都比较简单,文中就不一一列举
@RestController
@RequestMapping(value = "/auth")
public class AuthController {
@Autowired
private AuthService authService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtService jwtService;
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping(value = "/register")
public ResponseEntity createUser(@RequestBody UserCredential credential) {
credential.setPassword(passwordEncoder.encode(credential.getPassword()));
authService.save(credential);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
@PostMapping(value = "/token")
public ResponseEntity<String> generateToken(@RequestBody AuthRequest authRequest) {
final Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword()));
if (authenticate.isAuthenticated()) {
final String token = jwtService.generateToken(authRequest.getUsername());
return ResponseEntity.status(HttpStatus.OK).body(token);
} else {
throw new RuntimeException("invalid access");
}
}
@GetMapping(value = "/validate")
public ResponseEntity validateToken(@RequestParam String token) {
jwtService.validateToken(token);
return ResponseEntity.status(HttpStatus.ACCEPTED).build();
}
}
新建一个SpringBoot项目,命名为springcloud-gateway
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webfluxartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-apiartifactId>
<version>${jjwt.version}version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-implartifactId>
<version>${jjwt.version}version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-jacksonartifactId>
<version>${jjwt.version}version>
dependency>
@Component
public class AuthenticationFilter extends AbstractGatewayFilterFactory<AuthenticationFilter.Config> {
@Autowired
private RouteValidator validator;
public AuthenticationFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return ((exchange, chain) -> {
if (validator.isSecured.test(exchange.getRequest())) {
if (!exchange.getRequest().getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
throw new RuntimeException("missing authorization header");
}
String authHeader = exchange.getRequest().getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
if (null != authHeader && authHeader.startsWith("Bearer ")) {
authHeader = authHeader.substring(7);
}
try {
JwtUtil.validateToken(authHeader);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("un authorized access to application");
}
}
return chain.filter(exchange);
});
}
public static class Config {
}
}
spring:
application:
name: CLOUD-GATEWAY
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: provider_routh
uri: lb://CLOUD-PROVIDER-SERVER
predicates:
- Path=/provider/server/**
filters:
- AuthenticationFilter
- id: auth_routh
uri: lb://CLOUD-AUTH-SERVER
predicates:
- Path=/auth/**
依次启动注册中心服务、网关服务、鉴权服务和业务提供服务
postman发起post请求http://localhost:8000/auth/token获取token
postman发起get请求http://localhost:8000/provider/server/info,将上面获取的token携带上,如下配置
Type类型选择No Auth,再次发起请求
查看后台日志会发现是因为没有token
说明:本文只是简单实现了gateway实现统一鉴权功能,有些地方还需要小伙伴自行优化,例如没有token异常提示可以返回给前端