写点什么

10 分钟搞定 OAuth2

  • 2021 年 11 月 10 日
  • 本文字数:5037 字

    阅读完需:约 17 分钟

标准中注册的声明(建议但不强制使用) :


  • iss: jwt 签发者

  • sub: jwt 所面向的用户

  • aud: 接收 jwt 的一方

  • exp: jwt 的过期时间,这个过期时间必须要大于签发时间

  • nbf: 定义在什么时间之前,该 jwt 都是不可用的.

  • iat: jwt 的签发时间

  • jti: jwt 的唯一身份标识,主要用来作为一次性 token,从而回避重放攻击。


公共的声明 :


公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.


私有的声明 :


私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为 base64 是对称解密的,意味着该部分信息可以归类为明文信息。


定义一个 payload:


{


"sub": "1234567890",


"name": "John Doe",


"admin": true


}


然后将其进行 base64 加密,得到 Jwt 的第二部分。


eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9


signature


jwt 的第三部分是一个签证信息,这个签证信息由三部分组成:


  • header (base64 后的)

  • payload (base64 后的)

  • secret 这个部分需要 base64 加密后的 header 和 base64 加密后的 payload 使用.连接组成的字符串,然后通过 header 中声明的加密方式进行加盐 secret 组合加密,然后就构成了 jwt 的第三部分。


// javascript


var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);


var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ


将这三部分用.连接成一个完整的字符串,构成了最终的 jwt:


eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ


注意:secret 是保存在服务器端的,jwt 的签发生成也是在服务器端的,secret 就是用来进行 jwt 的签发和 jwt 的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个 secret, 那就意味着客户端是可以自我签发 jwt 了。


二、案例 demo


JWT 的概念讲完了,接下来就给大家详细的介绍一下代码的具体实现,客户端和服务器调用的流程,可以参照下面过程:



引入 JWT 和 Spring Security 依赖


<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>


添加 Web 配置文件,我们需要将除了登陆授权以外的接口,都进行过滤拦截,校验 Token 的合法性。


@Configuration


@EnableWebSecurity


@EnableGlobalMethodSecurity(prePostEnabled = true)


public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


@Autowired


private UserDetailsService userDetailsService;


@Autowired


public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {


authenticationManagerBuilder


// 设置 UserDetailsService


.userDetailsService(userDetailsService)


// 使用 BCrypt 进行密码的 hash


.passwordEncoder(passwordEncoder());


}


// 装载 BCrypt 密码编码器


@Bean


public PasswordEncoder passwordEncoder() {


return new BCryptPasswordEncoder();


}


@Bean


public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {


return new JwtAuthenticationTokenFilter();


}


@Override


protected void configure(AuthenticationManagerBuilder auth) throws Exception {


auth.userDetailsService(userDetailsService);


}


@Bean


@Override


public AuthenticationManager authenticationManagerBean() throws Exception {


return super.authenticationManagerBean();


}


@Override


protected void configure(HttpSecurity httpSecurity) throws Exception {


httpSecurity


// 由于使用的是 JWT,我们这里不需要 csrf


.csrf().disable()


// 基于 token,所以不需要 session


.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()


.authorizeRequests()


.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()


// 允许对于网站静态资源的无授权访问


.antMatchers(


HttpMethod.GET,


"/",


"/*.html",


"/favicon.ico",


"/**/*.html",


"/**/*.css",


"/**/*.js"


).permitAll()


// 授权接口放通 token 校验


.antMatchers("/authority/**/authorization/").permitAll()


// 除上面外的所有请求全部需要鉴权认证


.anyRequest().authenticated();


// 添加 JWT filter


httpSecurity.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);


// 禁用缓存


httpSecurity.headers().cacheControl();


}


}


Web 配置文件中我们可以看到,还需要 UserDetailsService 和 JwtAuthenticationTokenFilter。UserDetailsService 是 Spring Security 内部接口,我们需要实现该接口的 loadUserByUsername 方法,将查询到 username 和 password 返回,具体代码如下所示:


@Slf4j


@Service


public class UserDetailServiceImpl implements UserDetailsService {


@Autowired


private TamadbUserMapper userMapper;


@Override


public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {


TamadbUser userPo = userMapper.getUserBaseInfo(Integer.valueOf(userName));


if (userPo == null) {


log.error("loadUserByUsername--->userName:{}不存在", userName);


throw new UsernameNotFoundException("用户名不存在");


}


SysUserPo user = new SysUserPo();


user.setUsername(userPo.getId() + "");


user.setPassword(userPo.getPassword());


BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();


final String rawPassword = user.getPassword();


user.setPassword(encoder.encode(rawPassword));


return user;


}


}


userMapper.getUserBaseInfo 方法就是一个 dao,用来查询数据库的用户信息,因为 WebSecurityConfig 配置文件,对密码配置了 BCryptPasswordEncoder 加密,但是数据库存储的是 md5 生成的密码,所以我们需要对密码进行等价加密。


我们接着来看一下 JwtAuthenticationTokenFilter 过滤器的内容:


@Component


@Log4j2


public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {


@Autowired(required = false)


private UserDetailsService userDetailsService;


@Value("${jwt.header}")


private String header;


@Value("${jwt.tokenHead}")


private String tokenHead;


@Autowired


private JwtTokenUtil jwtTokenUtil;


@Override


protected void doFilterInternal(


HttpServletRequest request, HttpServletResponse response,


FilterChain chain) throws ServletException, IOException {


String authHeader = request.getHeader(this.header);


if (authHeader != null && authHeader.startsWith(tokenHead) && authHeader.length() > tokenHead.length() + 1) {


// The part after "Bearer "


final String authToken = authHeader.substring(tokenHead.length() + 1);


String username = jwtTokenUtil.getUsernameFromToken(authToken);


log.info("checking authentication,username:{},authToken:{}", username, authToken);


// 校验 token 是否有效合法


if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {


UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);


// 校验 token 是否过期


if (jwtTokenUtil.validateToken(authToken, userDetails)) {


UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(


userDetails, null, userDetails.getAuthorities());


authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(


request));


log.info("authenticated user " + username + ", setting security context");


SecurityContextHolder.getContext().setAuthentication(authentication);


} else {


log.error("token 过期,token:{}", authToken);


}


} else {


log.error("token 失效,无法获取到用户信息,token:{}", authHeader);


}


}


chain.doFilter(request, response);


}


}


过滤器就做一件事情,获取 Http 头部的 Token 信息,然后通过 jwtTokenUtil 解密 Token,获取用户信息,最后检验 Token 是否过期。


我们最后来看看 jwtTokenUtil 工具类中,是如何生成、解密 Token 的。


@Component


@Log4j2


public class JwtTokenUtil implements Serializable {


private static final long serialVersionUID = -3301605591108950415L;


/**


  • 用户 id


*/


private static final String CLAIM_KEY_USERNAME = "sub";


/**


  • 用户登录信息


*/


private static final String AUTHORITY_USER_DETAIL = "detail";


/**


  • token 创建时间


*/


private static final String CLAIM_KEY_CREATED = "created";


@Value("${jwt.secret}")


private String secret;


@Value("${jwt.expiration.pc.access}")


private Long pcAccessExpiration;


@Value("${jwt.expiration.pc.refresh}")


private Long pcRefreshExpiration;


@Value("${jwt.expiration.wechat.access}")


private Long weChatAccessExpiration;


@Value("${jwt.expiration.wechat.refresh}")


private Long weChatRefreshExpiration;


/**


  • 获取用户 token

  • @param token

  • @return


*/


public String getUsernameFromToken(String token) {


String username;


try {


final Claims claims = getClaimsFromToken(token);


username = claims.getSubject();


} catch (Exception e) {


username = null;


}


return username;


}


/**


  • 获取用户 token

  • @param token

  • @return


*/


public AuthorityUserDto getUserDetailFromToken(String token) {


AuthorityUserDto detail;


try {


final Claims claims = getClaimsFromToken(token);


Object detailObject = claims.get(AUTHORITY_USER_DETAIL);


Gson gson = new Gson();


// 解析 json


detail = gson.fromJson(gson.toJson(detailObject), AuthorityUserDto.class);


} catch (Exception e) {


detail = null;


}


return detail;


}


/**


  • 获取 token 的创建时间

  • @param token

  • @return


*/


public Date getCreatedDateFromToken(String token) {


Date created;


try {


final Claims claims = getClaimsFromToken(token);


created = new Date((Long) claims.get(CLAIM_KEY_CREATED));


} catch (Exception e) {


created = null;


}


return created;


}


/**


  • 获取 token 的过期时间

  • @param token

  • @return


*/


public Date getExpirationDateFromToken(String token) {


Date expiration;


try {


final Claims claims = getClaimsFromToken(token);


expiration = claims.getExpiration();


【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


} catch (Exception e) {


expiration = null;


}


return expiration;


}


/**


  • 调用 jar 生成 token 令牌

  • @param token

  • @return


*/


private Claims getClaimsFromToken(String token) {


Claims claims;


try {


claims = Jwts.parser()


.setSigningKey(secret)


.parseClaimsJws(token)


.getBody();


} catch (Exception e) {


log.error("解析 token 是失败:错误信息:{}", com.fourkmiles.common.util.ExceptionUtil.formatException(e));


claims = null;


}


return claims;


}


/**


  • 生成过期时间

  • @param tokenExpirationDto

  • @return


*/


private Date generateExpirationDate(TokenExpirationDto tokenExpirationDto) {


Date expirationDate = null;


ChannelEnum channelEnum = tokenExpirationDto.getChannelEnum();


TokenEnum tokenEnum = tokenExpirationDto.getTokenEnum();


if (channelEnum.getType() == ChannelEnum.PC_CHANNEL.getType()) {


if (tokenEnum.getType() == TokenEnum.ACCESS_TOKEN.getType()) {


expirationDate = new Date(System.currentTimeMillis() + pcAccessExpiration * 1000);


} else {


expirationDate = new Date(System.currentTimeMillis() + pcRefreshExpiration * 1000);


}


} else {


if (tokenEnum.getType() == TokenEnum.ACCESS_TOKEN.getType()) {


expirationDate = new Date(System.currentTimeMillis() + weChatAccessExpiration * 1000);


} else {


expirationDate = new Date(System.currentTimeMillis() + weChatRefreshExpiration * 1000);


}


}


return expirationDate;


}


/**


  • 校验 token 是否过期

评论

发布
暂无评论
10分钟搞定OAuth2