项目实战(JWT登录,认证信息中的当事人信息,跨域请求和预检信息)

50. 关于JWT(续)

JWT是不安全的,因为在不知道secretKey的情况下,任何JWT都是可以解析出Header、Payload部分的,这2部分的数据并没有做任何加密处理,所以,如果JWT数据被暴露,则任何人都可以从中解析出Header、Payload中的数据!

至于JWT中的secretKey,及生成JWT时使用的算法,是用于对Header、Payload执行签名算法的,JWT中的Signature是用于验证JWT真伪的。

当然,如果你认为有必要的话,可以自行另外使用加密算法,将Payload中应该封装的数据先加密,再用于生成JWT!

另外,如果JWT数据被泄露,他人使用有效的JWT是可以正常使用的!所以,通常,在相对比较封闭的操作系统(例如智能手机的操作系统)中,JWT的有效时间可以设置得很长,但是,不太封闭的操作系统(例如PC端的操作系统)中,JWT的有效时间应该相对较短。

所以,在JWT时,需要注意:

  • 根据你所需的安全性,来设置JWT的有效时间
  • 不要在JWT中存放敏感数据,例如:手机号码、身份证号码、明文密码
  • 如果一定要在JWT中存放敏感数据,应该自行使用加密算法处理过后再用于生成JWT

51. 登录成功时生成并响应JWT

在使用JWT的项目,用户登录就相当于现实生活乘车之前购买火车票的过程,所以,当用户登录成功时,需要生成对应的JWT数据,并响应到客户端。

首先,需要修改IAdminService接口中处理登录的抽象方法的声明,将返回值类型改为String,表示将返回成功登录的JWT数据:

/**
 * 管理员登录
 *
 * @param adminLoginDTO 封装了管理员的登录信息的对象
 * @return 成功登录的JWT数据
 */
String login(AdminLoginDTO adminLoginDTO);

然后,在AdminServiceImpl实现类中,也修改重写的方法的声明,并且,在登录成功后,生成、返回JWT数据:

log.debug("准备生成JWT数据");
Map<String, Object> claims = new HashMap<>();
// claims.put("id", null); // 向JWT中封装id
claims.put("username", adminLoginDTO.getUsername()); // 向JWT中封装username

String secretKey = "kns439a}fdLK34jsmfd{MF5-8DJSsLKhJNFDSjn";
Date expirationDate = new Date(System.currentTimeMillis() + 10 * 24 * 60 * 60 * 1000);
String jwt = Jwts.builder()
        .setHeaderParam("alg", "HS256")
        .setHeaderParam("typ", "JWT")
        .setClaims(claims)
        .setExpiration(expirationDate)
        .signWith(SignatureAlgorithm.HS256, secretKey)
        .compact();
log.debug("返回JWT数据:{}", jwt);
return jwt;

提示:以上代码并不是最终版本。

最后,在AdminController中,还需要响应JWT数据:

// http://localhost:9081/admins/login
@PostMapping("/login")
public JsonResult<String> login(AdminLoginDTO adminLoginDTO) {
    log.debug("开始处理【管理员登录】的请求,参数:{}", adminLoginDTO);
    String jwt = adminService.login(adminLoginDTO);
    return JsonResult.ok(jwt);
}

完成后,重启项目,可以在API文档中测试访问,当登录成功后,响应的结果大致是:

{
  "state": 20000,
  "data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjY0ODk1NjMsInVzZXJuYW1lIjoic3VwZXJfYWRtaW4ifQ.T5wnIVFk-AhvxPETloDsSgx46vdV45Y3BRk1_0oc3CM"
}

关于处理认证的细节

当调用了AuthenticationManager对象的authenticate()方法,且通过认证后,此方法将返回Authentication接口类型的对象,此对象的具体类型是UsernamePasswordAuthenticationToken,此对象中包含名为Principal(当事人)的属性,值为UserDetailsService对象中loadUserByUsername()返回的对象!

另外,目前在UserDetailsServiceImpl中返回的UserDetails接口类型的对象是User类型的,此类型没有id属性,如果需要向JWT中封装id甚至其它属性,必须自定义类,继承自User或实现UserDetails接口,在自定义类中补充声明所需的属性,并在UserDetailsServiceImpl中返回自定义类的对象,则处理认证通过后,返回的Authentication中的Principal就是自定义类的对象!

security包中创建AdminDetails类,继承自User对其进行扩展:

@Setter
@Getter
@EqualsAndHashCode
@ToString(callSuper = true)
public class AdminDetails extends User {

    private Long id;

    public AdminDetails(String username, String password, boolean enabled,
                        Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled,
                true, true, true,
                authorities);
    }

}

UserDetailsServiceImpl中,调整为返回AdminDetails类型的对象:

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    log.debug("Spring Security调用了loadUserByUsername()方法,参数:{}", s);
    AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
    log.debug("从数据库查询与用户名【{}】匹配的管理员信息:{}", s, loginInfo);

    if (loginInfo == null) {
        log.debug("此用户名【{}】不存在,即将抛出异常");
        String message = "登录失败,用户名不存在!";
        throw new BadCredentialsException(message);
    }

    // ===== 以下是调整的内容 =====
    List<GrantedAuthority> authorities = new ArrayList<>();
    GrantedAuthority authority = new SimpleGrantedAuthority("这是一个山寨的权限标识");
    authorities.add(authority);

    AdminDetails adminDetails = new AdminDetails(
            loginInfo.getUsername(), loginInfo.getPassword(),
            loginInfo.getEnable() == 1, authorities);
    adminDetails.setId(loginInfo.getId());

    log.debug("即将向Spring Security返回UserDetails接口类型的对象:{}", adminDetails);
    return adminDetails;
}

经过以上调整,当AuthenticationManager执行authenticate()认证方法后,如果登录成功,返回的Authentication中的Principal就是以上返回的AdminDetails对象,则可以从中获取idusername等数据,用于生成JWT数据,则在AdminServiceImpl中的login()方法中:

@Override
public String login(AdminLoginDTO adminLoginDTO) {
    log.debug("开始处理【管理员登录】的业务,参数:{}", adminLoginDTO);
    // 调用AuthenticationManager对象的authenticate()方法处理认证
    Authentication authentication
            = new UsernamePasswordAuthenticationToken(
                    adminLoginDTO.getUsername(), adminLoginDTO.getPassword());
    Authentication authenticateResult
            = authenticationManager.authenticate(authentication);
    log.debug("执行认证成功,AuthenticationManager返回:{}", authenticateResult);
    Object principal = authenticateResult.getPrincipal();
    log.debug("认证结果中的Principal数据类型:{}", principal.getClass().getName());
    log.debug("认证结果中的Principal数据:{}", principal);
    AdminDetails adminDetails = (AdminDetails) principal;

    log.debug("准备生成JWT数据");
    Map<String, Object> claims = new HashMap<>();
    claims.put("id", adminDetails.getId()); // 向JWT中封装id
    claims.put("username", adminDetails.getUsername()); // 向JWT中封装username

    String secretKey = "kns439a}fdLK34jsmfd{MF5-8DJSsLKhJNFDSjn";
    Date expirationDate = new Date(System.currentTimeMillis() + 10 * 24 * 60 * 60 * 1000);
    String jwt = Jwts.builder()
            .setHeaderParam("alg", "HS256")
            .setHeaderParam("typ", "JWT")
            .setClaims(claims)
            .setExpiration(expirationDate)
            .signWith(SignatureAlgorithm.HS256, secretKey)
            .compact();
    log.debug("返回JWT数据:{}", jwt);
    return jwt;
}

至此,当客户端向服务器端提交登录请求,且登录成功后,将得到服务器端响应的JWT数据,此JWT中包含了idusername

解析JWT

当客户端已经登录成功并得到JWT,相当于现实生活中某人已经成功购买到了火车票,接下来,此人应该携带火车票去乘车,在程序中,就表现为:客户端应该携带JWT向服务器端提交请求。

关于客户端携带JWT数据,业内惯用的做法是客户端应该将JWT放在请求头(Request Headers)中名为Authorization的属性中。

在服务器端,通常使用过滤器组件来解析JWT数据。

在项目的根包下创建JwtAuthorizationFilter

package cn.tedu.csmall.passport.filter;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * JWT认证过滤器
 *
 * <p>Spring Security框架会自动从SecurityContext读取认证信息,如果存在有效信息,则视为已登录,否则,视为未登录</p>
 * <p>当前过滤器应该尝试解析客户端可能携带的JWT,如果解析成功,则创建对应的认证信息,并存储到SecurityContext中</p>
 *
 * @author java@tedu.cn
 * @version 0.0.1
 */
@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    public static final int JWT_MIN_LENGTH = 100;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        // 尝试获取客户端提交请求时可能携带的JWT
        String jwt = request.getHeader("Authorization");
        log.debug("接收到JWT数据:{}", jwt);

        // 判断是否获取到有效的JWT
        if (!StringUtils.hasText(jwt) || jwt.length() < JWT_MIN_LENGTH) {
            // 直接放行
            log.debug("未获取到有效的JWT数据,将直接放行");
            filterChain.doFilter(request, response);
            return;
        }

        // 尝试解析JWT,从中获取用户的相关数据,例如id、username等
        log.debug("将尝试解析JWT……");
        String secretKey = "kns439a}fdLK34jsmfd{MF5-8DJSsLKhJNFDSjn";
        Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
        Long id = claims.get("id", Long.class);
        String username = claims.get("username", String.class);
        log.debug("从JWT中解析得到数据:id={}", id);
        log.debug("从JWT中解析得到数据:username={}", username);

        // 将根据从JWT中解析得到的数据来创建认证信息
        List<GrantedAuthority> authorities = new ArrayList<>();
        GrantedAuthority authority = new SimpleGrantedAuthority("这是一个山寨的权限标识");
        authorities.add(authority);
        Authentication authentication = new UsernamePasswordAuthenticationToken(
                username, null, authorities);

        // 将认证信息存储到SecurityContext中
        SecurityContext securityContext = SecurityContextHolder.getContext();
        securityContext.setAuthentication(authentication);

        // 放行
        filterChain.doFilter(request, response);
    }

}

完成后,还需要在SecurityConfiguration中自动装配自定义的JWT过滤器:

@Autowired
JwtAuthorizationFilter jwtAuthorizationFilter;

并在configurer()方法中补充:

// 将自定义的JWT过滤器添加在Spring Security框架内置的过滤器之前
http.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class);

52. 关于认证信息中的Principal

关于SecurityContext中的认证信息,应该包含当事人(Principal)和权限(Authorities),其中,当事人(Principal)被声明为Object类型的,则可以使用任意数据类型作为当事人!

在使用了Spring Security框架的项目中,当事人的数据是可以被注入到处理请求的方法中的!所以,使用哪种数据作为当事人,主要取决于“你在编写控制器中处理请求的方法时,需要通过哪些数据来区分当前登录的用户”。

通常,使用自定义的数据类型作为当事人,并在此类型中封装关键数据,例如idusername等。

则在security包下创建LoginPrincipal类:

@Data
public class LoginPrincipal implements Serializable {
    private Long id;
    private String username;
}

在JWT过滤器创建认证信息时,使用以上类型的对象作为认证信息中的当事人:

LoginPrincipal loginPrincipal = new LoginPrincipal(); // 新增
loginPrincipal.setId(id); // 新增
loginPrincipal.setUsername(username); // 新增

// 注意:以下调用构造方法时,第1个参数是以上创建的对象
Authentication authentication = new UsernamePasswordAuthenticationToken(
        loginPrincipal, null, authorities);

完成后,在当前项目任何控制器中任何处理请求的方法上,都可以添加@AuthenticationPrincipal LoginPrincipal loginPrincipal参数(与原有的其它参数不区分先后顺序),此参数的值就是以上过滤器中存入到认证信息中的当事人,所以,可以通过这种做法,在处理请求时识别当前登录的用户:

@ApiOperation("删除管理员")
@ApiOperationSupport(order = 200)
@ApiImplicitParam(name = "id", value = "管理员id", required = true, dataType = "long")
@PostMapping("/{id:[0-9]+}/delete")
public JsonResult<Void> delete(@PathVariable Long id,
        // ===== 以下是新增的方法参数 =====
        @ApiIgnore @AuthenticationPrincipal LoginPrincipal loginPrincipal) {
    log.debug("开始处理【删除管理员】的请求,参数:{}", id);
    log.debug("当前登录的当事人:{}", loginPrincipal); // 新增,可以控制台观察数据
    adminService.delete(id);
    return JsonResult.ok();
}

53. 关于CORS与PreFlight

如果客户端向服务器端提交请求,在跨域的前提下,如果提交的请求配置了请求头中的非典型参数,例如配置了Authorization,此请求会被视为“复杂请求”,则会要求执行“预检”(PreFlight),如果预检不通过,则会导致跨域请求错误!

关于预检,浏览器会自动向服务器端提交OPTIONS类型的请求执行预检,为了确保预检通过,不影响处理正常的请求,需要在SecurityConfigurationconfigurer()方法中对预检请求放行,可以采取的解决方案有:

http.authorizeRequests()
                .antMatchers(urls)
                .permitAll()

                // 以下2行代码是用于对预检的OPTIONS请求直接放行的
                .antMatchers(HttpMethod.OPTIONS, "/**")
                .permitAll()

                .anyRequest()
                .authenticated();

或者,也可以:

http.cors(); // 启用Spring Security框架的处理跨域的过滤器,此过滤器将放行跨域请求,包括预检的OPTIONS请求

则客户端可以携带复杂请求头进行访问:

loadAdminList() {
  console.log('loadAdminList ...');
  let url = 'http://localhost:9081/admins';
  console.log('url = ' + url);
  this.axios
      .create({
        'headers': {
          'Authorization': localStorage.getItem('jwt')
        }
      })
      .get(url).then((response) => {
    let responseBody = response.data;
    console.log(responseBody);
    this.tableData = responseBody.data;
  });
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

专注摸鱼的汪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值