OAuth2.0是当前业界标准的授权协议,它的核心是若干个针对不同场景的令牌颁发和管理流程;而JWT是一种轻量级、自包含的令牌,可用于在微服务间安全地传递用户信息。OAuth 2.0的模式一共有四种,这里只假设客户直接访问公司自己的门户网站,即第一方Web应用,可以选择OAuth 2.0的资源拥有者凭据模式。 OAuth2令牌 + JWT混合模式,IDP(Identity Provider)要支持OAuth 2.0 授权协议处理,及从OAuth2令牌到JWT令牌的转换。最后以代码方式实现了授权、认证过程。
先做一个概览,下面是Access token + JWT混合模式流程图:
Client : 资源拥有者
IDP:授权服务
Providers: 受保护资源
引入依赖
>
>
>org.springframework.security.oauth >
>spring-security-oauth2 >
>
>
Authorization 配置类:
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
private final AuthenticationManager authenticationManager;
public OAuth2AuthorizationConfig(AuthenticationConfiguration authenticationConfiguration) throws Exception {
this.authenticationManager = authenticationConfiguration.getAuthenticationManager();
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpointsConfigurer) {
endpointsConfigurer.authenticationManager(authenticationManager);
}
@Override
public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception {
clientDetailsServiceConfigurer.inMemory()
.withClient("clientapp")
.secret("{bcrypt}$2a$10$OL6aAqldM9QMoK/D3Y0xA.KIaWw9kOBs83exhr4DUACcHq5ZeCI7C")
.authorizedGrantTypes("password", "refresh_token")
.scopes("read_userinfo", "read_contacts");
}
}
获取access_token实验结果:
将Resource server 配置类与Authorization集成在同一个ms中,使用access token直接获取资源。但大型分布式微服务的授权和认证还是推荐 OAuth2 + JWT混合模式。
@Configuration
@EnableResourceServer
public class OAuth2ResourceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.requestMatchers()
.antMatchers("/api/**");
}
}
调用资源API:
@Controller
public class UserController {
@RequestMapping("/api/userinfo")
public ResponseEntity<UserInfo> getUserInfo() {
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String email = user.getUsername() + "@spring2go.com";
UserInfo userInfo = new UserInfo();
userInfo.setName(user.getUsername());
userInfo.setEmail(email);
return ResponseEntity.ok(userInfo);
}
}
Java中对JWT的支持可以使用JJWT(实现了JOSE规范)开源库;
import依赖:
>
>com.auth0 >
>java-jwt >
>${java-jwt.version} >
>
>
>io.jsonwebtoken >
>jjwt-api >
>${jjwt-api.version} >
>
>
>io.jsonwebtoken >
>jjwt-impl >
>${jjwt-api.version} >
>
>
>io.jsonwebtoken >
>jjwt-gson >
>${jjwt-api.version} >
>
jwt的生成:
private static final String JWT_SECRET = "test_jwt_secret_test_jwt_secret_test_jwt_secret";
public static SecretKey generalKey() {
byte[] encodeKey = JWT_SECRET.getBytes(StandardCharsets.UTF_8);
//这里只是简单的实验,生产中要使用非对称加密~
SecretKey key = new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256");
return key;
}
/**
* 签发JWT, 创建token的方法
*
* @param id jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
* @param iss jwt签发者
* @param subject jwt所面向的用户, payload中记录的public claims. 即为JWTSubject的信息
* @param ttlMillis 有效期,单位毫秒
* @return token
*/
public static String createJWT(String id, String iss, String subject, long ttlMillis) {
long currentMillis = System.currentTimeMillis();
Date now = new Date();
SecretKey secretKey = generalKey();
JwtBuilder builder = Jwts.builder()
.serializeToJsonWith(new GsonSerializer<>(new Gson()))
.setId(id)//身份标识
.setIssuer(iss)
.setSubject(subject)
.setIssuedAt(now)
.signWith(secretKey);
if (ttlMillis >= 0) {
long expMillis = currentMillis + ttlMillis;
Date expDate = new Date(expMillis);
builder.setExpiration(expDate);
}
return builder.compact();
}
jwt的校验:
public static JWTResult validateJWT(String jwtStr) {
JWTResult checkResult = new JWTResult();
Claims claims;
try {
claims = parseJWT(jwtStr);
checkResult.setSuccess(true);
checkResult.setClaims(claims);
} catch (ExpiredJwtException e) {
checkResult.setErrCode(JWT_ERR_CODE_EXPIRE);
checkResult.setSuccess(false);
} catch (Exception e) {
checkResult.setErrCode(JWT_ERR_CODE_FAIL);
checkResult.setSuccess(false);
}
return checkResult;
}
public static Claims parseJWT(String jwt) {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();//token中记录的payload数据
}