一、场景
前后端分离,数据交互该如何保证安全性?
某人:Token呀
移动端、小程序、H5等客户端同样是通过JSON数据传输,也涉及到身份校验问题
二、问题
Token到底如何实现,才是最好的方式?
身经百个项目(.(▼へ▼メ)装逼.) 见过不少项目是这样实现的
- token?我们不需要
- token写死的,后台提供给客户端(确实有不少)
- token动态,后台通过某些算法生成token,提供给客户端(这种方式不是不行,只是不够完美)
- 整个JSON数据加密传输,后台提供密钥(em….不评价)
- 等等
三、解决方案
今天要讲解的是采用 JWT,解决Token问题
#科普一下JWT
JSON Web Token (JWT) 是在网络应用间传递信息的一种基于JSON的开放标准 (RFC 7519),用于作为JSON对象在不同系统之间进行安全地信息传输。主要使用场景一般是用来在 身份提供者和服务提供者间传递被认证的用户身份信息。
JWT由三部分组成,依次如下:
- Header(头部) 包含两部分:token类型和采用的签名算法
- Payload(负载) 是Token携带的用户自定义内容,数据是base64,非加密,可破解,不能存敏感信息;内容越多,token越长
- Signature(签名) 将Header+Payload组成一个字符串,通过指定的签名算法进行计算,得到一个签名值,服务端可通过这个标志,来判断Token是否合法
#讲解Spring Security(附赠)
有JWT解决Token问题,再结合Spring Security 可以解决用户权限模块重重问题(真的很强大),当然除了Spring Security,还有Apache Shiro也可以解决权限,但没有Security强大,后续将写篇文章详解对比两者。
四、实现方式
pom.xml中引入 JWT 和 Spring Security 的依赖 jar
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
JWT工具类,生成Token、验证Token、刷新Token等
@Component
public class JwtTokenUtil {
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
// 生成token
public String generateToken(UserDetails userDetails) {
...
}
// 刷新token
public String refreshToken(String token) {
...
}
// 校验token
public Boolean validateToken(String token) {
...
}
}
Token过滤器, 处理接口的token
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader(Constant.HEADER_STRING );
if (authHeader != null && authHeader.startsWith(Constant.TOKEN_PREFIX )) {
final String authToken = authHeader.substring(Constant.TOKEN_PREFIX.length() );
String username = jwtTokenUtil.getUsernameFromToken(authToken);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
Spring Security的核心配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userService;
@Bean
public JwtTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtTokenFilter();
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
String[] urls = new String[]{"/user/login", "/user/register"};
httpSecurity.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// 允许注册和登录接口不需token访问
.antMatchers(urls).permitAll()
.anyRequest().authenticated();
httpSecurity.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
httpSecurity.headers().cacheControl();
}
}
权限测试,接口需要限制访问限制,则添加 @PreAuthorize
@RestController
public class RoleController {
/**
* 测试普通权限
*
* @return
*/
@PreAuthorize("hasAuthority('ROLE_NORMAL')")
@GetMapping(value="/normal/test")
public String test1() {
return "普通角色访问";
}
/**
* 测试管理员权限
*
* @return
*/
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
@GetMapping(value = "/admin/test")
public String test2() {
return "管理员访问";
}
}
需给用户添加角色才能访问,如下图 用户角色关系表
五、Token如何校验
注意:JWT 默认是不加密的,任何人都可以读到,可以复制一串token到 JWT在线解密
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJPd2F0ZXIiLCJjcmVhdGVkIjoxNTYzMzc2ODE2NTIzLCJleHAiOjE1NjMzODQwMTZ9.SuL4dRoweriLOnZzohcEzUwXf7kVSx9KnTIGtB7ffuBtlUUFS1T8il7_fxv3Gn1LkX5DGOawqNhG4ZWYxALDig
从上面可以看出,token存储的信息可以被解析出来的,所以大家不要把敏感信息存放这里
那token不会被篡改呢?不会,因为在token的三部分(Header、Payload、Signature) 中,Signature可以防止被篡改,这个密钥是只有服务端才知道
六、测试效果
- 登录成功后返回token
- 不带token的情况,会被拒绝访问
- 正确访问方式