1. 概述
在微服务项目中,需要对整个微服务系统进行权限校验,通常有两种方案,其一是每个微服务各自鉴权,其二是在网关统一鉴权,第二种方案只需要一次鉴权就行,避免了每个微服务重复鉴权的麻烦,本文以网关统一鉴权为例介绍如何搭建微服务鉴权项目。
本文案例中共有四个微服务模块,服务注册中心、网关服务、鉴权服务和业务提供者
案例中使用组件版本号如下:
组件 | 版本 |
---|---|
JDK | 11 |
SpringBoot | 2.7.9 |
SpringCloud | 2021.0.6 |
Mybatis-Plus | 3.5.3.1 |
jjwt | 0.11.5 |
2. 鉴权微服务
新建一个SpringBoot项目,命名为springcloud-auth-server
2.1. 引入核心依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mybatis-plus.version}</version>
</dependency>
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>${jjwt.version}</version>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>${jjwt.version}</version>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>${jjwt.version}</version>
</dependency>
<dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope>
</dependency>
2.2. 编写JWT业务类
@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);}
}
2.3. 编写配置类
@Configuration
@EnableWebSecurity
public class AuthConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.csrf().disable().authorizeHttpRequests().antMatchers("/auth/register", "/auth/token", "/auth/validate").permitAll();return http.build();}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic UserDetailsService userDetailsService() {return new CustomUserDetailsService();}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {return configuration.getAuthenticationManager();}@Beanpublic AuthenticationProvider authenticationProvider() {DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();authenticationProvider.setUserDetailsService(userDetailsService());authenticationProvider.setPasswordEncoder(passwordEncoder());return authenticationProvider;}
}
2.4. 编写Security用户认证
新建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();}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return username;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
编写CustomUserDetailsService类实现UserDetailsService接口
@Service
public class CustomUserDetailsService implements UserDetailsService {@Autowiredprivate AuthService authService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {Optional<UserCredential> credential = authService.findUserByUsername(username);return credential.map(CustomUserDetails::new).orElseThrow(() -> new UsernameNotFoundException("user not found"));}
}
说明:文中用到的用户认证类UserCredential只有用户名和密码字段,实体类、持久层接口和业务类接口都比较简单,文中就不一一列举
2.5. 编写权限Controller类
@RestController
@RequestMapping(value = "/auth")
public class AuthController {@Autowiredprivate AuthService authService;@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate JwtService jwtService;@Autowiredprivate 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();}
}
3. 网关微服务
新建一个SpringBoot项目,命名为springcloud-gateway
3.1. 引入核心依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>${jjwt.version}</version>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>${jjwt.version}</version>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>${jjwt.version}</version>
</dependency>
3.2. 编写权限过滤器类
@Component
public class AuthenticationFilter extends AbstractGatewayFilterFactory<AuthenticationFilter.Config> {@Autowiredprivate RouteValidator validator;public AuthenticationFilter() {super(Config.class);}@Overridepublic 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 {}
}
3.3. 编写application.yml配置
spring:application:name: CLOUD-GATEWAYcloud:gateway:discovery:locator:enabled: trueroutes:- id: provider_routhuri: lb://CLOUD-PROVIDER-SERVERpredicates:- Path=/provider/server/**filters:- AuthenticationFilter- id: auth_routhuri: lb://CLOUD-AUTH-SERVERpredicates:- Path=/auth/**
4. 测试
依次启动注册中心服务、网关服务、鉴权服务和业务提供服务
postman发起post请求http://localhost:8000/auth/token获取token
postman发起get请求http://localhost:8000/provider/server/info,将上面获取的token携带上,如下配置
Type类型选择No Auth,再次发起请求
查看后台日志会发现是因为没有token
说明:本文只是简单实现了gateway实现统一鉴权功能,有些地方还需要小伙伴自行优化,例如没有token异常提示可以返回给前端