一、背景
新的业务应用,提供登录、权限、离线和在线业务的能力。内网环境。
对于以上使用场景,考虑了其他的一些登录方案,比如常见的自己手写登录、基于spring security、jwt以及spring security jwt的解决方案,考虑到开发成本和技术成熟度,决定选择最后一种方案,并且在分析了jwt优缺点的基础上做了一些妥协和改造。
二、技术调研
1.jwt
jwt是一种无状态的前后端交互协议,概念和原理不做过多介绍,大致流程如下图
- 登录
登录完成返回给client一个token(身份签名)
- 验签
client访问服务端需带上token信息
2.spring security
三、技术方案
之所以选择jwt security方案,是因为结合我们的具体业务场景,并考虑两种方案的优缺点以及应用场景,单独的一种都不太符合我们的诉求:
- jwt:无状态,需要自己写认证和授权逻辑,并且对于用户修改密码后,服务端无法主动剔出颁发出去token(当然这个也不一定算得上缺点,仔细思考下stateless就知道了)
- spring security:对于表单登录有比较完善解决方案,对于前后端分离场景,需要自定义实现
所以我们选择了jwt spring security version(服务端)这种轻状态的前后端交互协议。
实现过程中需要考虑的细节:
- token过期,自动过期与主动过期(密码修改)
- 在token失效前访问,自动续期,返回新token
- 登录完成需要返回用户菜单列表
- 认证和授权失败错误码
四、代码实现
1.引入依赖
代码语言:javascript复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
2.jwttoken工具
代码语言:javascript复制public static String createToken(String username,String role) {
Map<String,Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, role);
String token = Jwts
.builder()
.setSubject(username)
.setClaims(map)
.claim("username",username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() EXPIRITION))
.signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact();
return token;
}
public static Claims checkJWT(String token) {
try {
final Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
3.认证过滤器
代码语言:javascript复制public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
super.setFilterProcessesUrl("/auth/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 从输入流中获取到登录的信息
try {
User loginUser = new ObjectMapper().readValue(request.getInputStream(), User.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword())
);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
// 成功验证后调用的方法
// 如果验证成功,就生成token并返回
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
System.out.println("jwtUser:" jwtUser.toString());
String role = "";
Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
for (GrantedAuthority authority : authorities){
role = authority.getAuthority();
}
String token = JwtUtil.createToken(jwtUser.getUsername(), role);
//String token = JwtTokenUtils.createToken(jwtUser.getUsername(), false);
// 返回创建成功的token
// 但是这里创建的token只是单纯的token
// 按照jwt的规定,最后请求的时候应该是 `Bearer token`
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
String tokenStr = JwtUtil.TOKEN_PREFIX token;
response.setHeader("token",tokenStr);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.getWriter().write("authentication failed, reason: " failed.getMessage());
}
}
4.授权过滤器
代码语言:javascript复制public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String tokenHeader = request.getHeader(JwtUtil.TOKEN_HEADER);
// 如果请求头中没有Authorization信息则直接放行了,交给后边的认证拦截
if (tokenHeader == null || !tokenHeader.startsWith(JwtUtil.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// 如果请求头中有token,则进行解析,并且设置认证信息
SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
super.doFilterInternal(request, response, chain);
}
// 这里从token中获取用户信息并新建一个token
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
String token = tokenHeader.replace(JwtUtil.TOKEN_PREFIX, "");
//todo 如果有篡改token,捕获SignatureException并返回
String username = JwtUtil.getUsername(token);
String role = JwtUtil.getUserRole(token);
if (username != null){
return new UsernamePasswordAuthenticationToken(username, null,
Collections.singleton(new SimpleGrantedAuthority(role))
);
}
return null;
}
}
5.权限拦截配置
代码语言:javascript复制@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("jwtUserDetailService")
private UserDetailsService userDetailsService;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
// 测试用资源,需要验证了的用户才能访问
.antMatchers("/tasks/**")
.authenticated()
.antMatchers(HttpMethod.DELETE, "/tasks/**")
.hasRole("ADMIN")
// 其他都放行了
.anyRequest().permitAll()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// 不需要session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
.authenticationEntryPoint(new JwtAuthenticationEntryPoint());
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
总结
本篇实现基于spring-security能力,很好的复用了其权限模块,当然我们也可以完全自己实现,把权限认证和授权逻辑通过拦截器的方式嵌入到请求流中,并且在一些内部系统或者登录态要求不是很强的场景都可以使用jwt方案来实现简单的认证和授权,当然jwt相比于oauth2.0更像是其其中一个小而美的模块能力,一些复杂的场景或者平台级别的应用,建议使用CAS来做统一门面,或者oauth2.0来做对外权限认证。