上一篇已经讲了微服务组件中的 路由网关(Zuul),但是未介绍服务认证相关,本章主要讲解基于
Spring Security
与JJWT
实现 JWT(JSON Web Token)为接口做授权处理…
JWT(JSON Web Token), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
通常情况下,把API直接暴露出去是风险很大的,不说别的,直接被机器攻击就喝一壶的。那么一般来说,对API要划分出一定的权限级别,然后做一个用户的鉴权,依据鉴权结果给予用户开放对应的API。目前,比较主流的方案有几种:
OAuth
OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一服务上存储的私密的资源(如照片,视频),而无需将用户名和密码提供给第三方应用。
OAuth 允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容
Cookie/Session Auth
Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使cookie在一定时间内有效,基于session方式认证势必会对服务器造成一定的压力(内存存储),不易于扩展(需要处理分布式session),跨站请求伪造的攻击(CSRF)
1.相比于session,它无需保存在服务器,不占用服务器内存开销。
2.无状态、可拓展性强:比如有3台机器(A、B、C)组成服务器集群,若session存在机器A上,session只能保存在其中一台服务器,此时你便不能访问机器B、C,因为B、C上没有存放该Session,而使用token就能够验证用户请求合法性,并且我再加几台机器也没事,所以可拓展性好就是这个意思。
3.前后端分离,支持跨域访问。
{ "iss": "JWT Builder",
"iat": 1416797419,
"exp": 1448333419,
"aud": "www.battcn.com",
"sub": "[email protected]",
"GivenName": "Levin",
"Surname": "Levin",
"Email": "[email protected]",
"Role": [ "ADMIN", "MEMBER" ] }
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷、签名(上图依次排序)
JWT Token生成器:https://jwt.io/
无效Token
有效Token
有优点就会有缺点,是否适用应该考虑清楚,而不是技术跟风
TokenProperties 与
application.yml
资源的key映射,方便使用
@Configuration
@ConfigurationProperties(prefix = "battcn.security.token")
public class TokenProperties {
/** * {@link com.battcn.security.model.token.Token} token的过期时间 */
private Integer expirationTime;
/** * 发行人 */
private String issuer;
/** * 使用的签名KEY {@link com.battcn.security.model.token.Token}. */
private String signingKey;
/** * {@link com.battcn.security.model.token.Token} 刷新过期时间 */
private Integer refreshExpTime;
// get set ...
}
Token生成的类
@Component
public class TokenFactory {
private final TokenProperties properties;
@Autowired
public TokenFactory(TokenProperties properties) {
this.properties = properties;
}
/** * 利用JJWT 生成 Token * @param context * @return */
public AccessToken createAccessToken(UserContext context) {
Optional.ofNullable(context.getUsername()).orElseThrow(()-> new IllegalArgumentException("Cannot create Token without username"));
Optional.ofNullable(context.getAuthorities()).orElseThrow(()-> new IllegalArgumentException("User doesn't have any privileges"));
Claims claims = Jwts.claims().setSubject(context.getUsername());
claims.put("scopes", context.getAuthorities().stream().map(Object::toString).collect(toList()));
LocalDateTime currentTime = LocalDateTime.now();
String token = Jwts.builder()
.setClaims(claims)
.setIssuer(properties.getIssuer())
.setIssuedAt(Date.from(currentTime.atZone(ZoneId.systemDefault()).toInstant()))
.setExpiration(Date.from(currentTime
.plusMinutes(properties.getExpirationTime())
.atZone(ZoneId.systemDefault()).toInstant()))
.signWith(SignatureAlgorithm.HS512, properties.getSigningKey())
.compact();
return new AccessToken(token, claims);
}
/** * 生成 刷新 RefreshToken * @param userContext * @return */
public Token createRefreshToken(UserContext userContext) {
if (StringUtils.isBlank(userContext.getUsername())) {
throw new IllegalArgumentException("Cannot create Token without username");
}
LocalDateTime currentTime = LocalDateTime.now();
Claims claims = Jwts.claims().setSubject(userContext.getUsername());
claims.put("scopes", Arrays.asList(Scopes.REFRESH_TOKEN.authority()));
String token = Jwts.builder()
.setClaims(claims)
.setIssuer(properties.getIssuer())
.setId(UUID.randomUUID().toString())
.setIssuedAt(Date.from(currentTime.atZone(ZoneId.systemDefault()).toInstant()))
.setExpiration(Date.from(currentTime
.plusMinutes(properties.getRefreshExpTime())
.atZone(ZoneId.systemDefault()).toInstant()))
.signWith(SignatureAlgorithm.HS512, properties.getSigningKey())
.compact();
return new AccessToken(token, claims);
}
}
配置文件,含token过期时间,秘钥,可自行扩展
battcn:
security:
token:
expiration-time: 10 # 分钟 1440
refresh-exp-time: 30 # 分钟 2880
issuer: http://blog.battcn.com
signing-key: battcn
WebSecurityConfig 是 Spring Security 关键配置,在Securrty中基本上可以通过定义过滤器去实现我们想要的功能.
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public static final String TOKEN_HEADER_PARAM = "X-Authorization";
public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login";
public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
public static final String MANAGE_TOKEN_BASED_AUTH_ENTRY_POINT = "/manage/**";
public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token";
@Autowired private RestAuthenticationEntryPoint authenticationEntryPoint;
@Autowired private AuthenticationSuccessHandler successHandler;
@Autowired private AuthenticationFailureHandler failureHandler;
@Autowired private LoginAuthenticationProvider loginAuthenticationProvider;
@Autowired private TokenAuthenticationProvider tokenAuthenticationProvider;
@Autowired private TokenExtractor tokenExtractor;
@Autowired private AuthenticationManager authenticationManager;
protected LoginProcessingFilter buildLoginProcessingFilter() throws Exception {
LoginProcessingFilter filter = new LoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler);
filter.setAuthenticationManager(this.authenticationManager);
return filter;
}
protected TokenAuthenticationProcessingFilter buildTokenAuthenticationProcessingFilter() throws Exception {
List list = Lists.newArrayList(TOKEN_BASED_AUTH_ENTRY_POINT,MANAGE_TOKEN_BASED_AUTH_ENTRY_POINT);
SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(list);
TokenAuthenticationProcessingFilter filter = new TokenAuthenticationProcessingFilter(failureHandler, tokenExtractor, matcher);
filter.setAuthenticationManager(this.authenticationManager);
return filter;
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(loginAuthenticationProvider);
auth.authenticationProvider(tokenAuthenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // 因为使用的是JWT,因此这里可以关闭csrf了
.exceptionHandling()
.authenticationEntryPoint(this.authenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point
.antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point
.and()
.authorizeRequests()
.antMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated() // Protected API End-points
.antMatchers(MANAGE_TOKEN_BASED_AUTH_ENTRY_POINT).hasAnyRole(RoleEnum.ADMIN.name())
.and()
.addFilterBefore(buildLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
由于JWT代码做了简单封装,包含内容较多,所以文章里只贴主要片段,需要完整代码可以直接从下面GIT获取
本章代码(battcn-jwt-service):https://git.oschina.net/battcn/battcn-cloud/tree/master/battcn-cloud-jwt/battcn-jwt-service
如有问题请及时与我联系