前后端分离JWT登录认证

一、自定义返回类

1. 状态码枚举

@Getter
public enum ResultCodeEnum {
    SUCCESS(200, "成功"),

    FAIL(400, "失败"),

    USERNAME_PWD_ERROR(201, "用户名或密码错误"),

    PARAM_ERROR(202, "参数不正确"),

    SERVICE_ERROR(203, "服务异常"),

    DATA_ERROR(204, "数据异常"),

    DATA_UPDATE_ERROR(205, "数据版本异常"),

    LOGIN_AUTH(208, "未登陆"),

    PERMISSION(209, "没有权限"),

    CODE_ERROR(210, "验证码错误"),

    //    LOGIN_MOBLE_ERROR(211, "账号不正确"),
    LOGIN_DISABLED_ERROR(212, "该用户已被禁用"),

    REGISTER_MOBLE_ERROR(213, "手机号已被使用"),

    LOGIN_AURH(214, "需要登录"),

    LOGIN_ACL(215, "没有权限"),

    URL_ENCODE_ERROR(216, "URL编码失败"),

    ILLEGAL_CALLBACK_REQUEST_ERROR(217, "非法回调请求"),

    TOKEN_ERROR(218, "token无效"),
    // TOKEN_ERROR(219, "token无效"),

    FETCH_USERINFO_ERROR(220, "获取用户信息失败"),
    ;
    private Integer code;
    private String msg;

    ResultCodeEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

2. 封装返回体类

这是封装一个返回数据,包括返回状态码、信息、返回体数据,如:

image-20220830093313043

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResponseResult<T> {
    /**
     * 返回状态码
     */
    private Integer code;
    /**
     * 返回信息
     */
    private String msg;
    /**
     * 返回体数据
     */
    private T data;

    public static <T> ResponseResult<T> build(T data) {
        ResponseResult<T> responseResult = new ResponseResult<>();
        if (data != null)
            responseResult.setData(data);
        return responseResult;
    }

    public static <T> ResponseResult<T> build(ResultCodeEnum resultCodeEnum, T body) {
        ResponseResult<T> responseResult = new ResponseResult<>();
        responseResult.setData(body);
        responseResult.setCode(resultCodeEnum.getCode());
        responseResult.setMsg(resultCodeEnum.getMsg());
        return responseResult;
    }

    public static <T> ResponseResult<T> build(Integer code, String msg) {
        ResponseResult<T> responseResult = build(null);
        responseResult.setCode(code);
        responseResult.setMsg(msg);
        return responseResult;
    }

    public static <T> ResponseResult<T> success(T data) {
        return build(ResultCodeEnum.SUCCESS, data);
    }

    public static <T> ResponseResult<T> success() {
        return success(null);
    }

    public static <T> ResponseResult<T> fail(T data) {
        return build(ResultCodeEnum.FAIL, data);
    }

    public static <T> ResponseResult<T> fail(ResultCodeEnum resultCodeEnum, T data) {
        return build(resultCodeEnum, data);
    }

    public static <T> ResponseResult<T> fail() {
        return fail(null);
    }

}

3. 封装response返回

使用流技术,向response中设置返回数据,

public class HttpResponse<T> {
    public static <T> void respBack(HttpServletRequest request, HttpServletResponse response, T result) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        // PrintWriter writer = response.getWriter();
        Writer out= new BufferedWriter(new OutputStreamWriter(response.getOutputStream()));
        out.write(new ObjectMapper().writeValueAsString(result));
        out.flush();
        out.close();
    }
}

上面的result返回类就可以放到里面返回,调用:

ResponseResult<Object> result = ResponseResult.fail(ResultCodeEnum.TOKEN_ERROR, null);
HttpResponse.respBack(null, response, result);

二、自定义全局异常

1. 自定义异常

@Data
public class MyException extends RuntimeException {
    /**
     * 异常状态码
     */
    private Integer code;

    /**
     * 自定义异常状态码和信息
     *
     * @param msg
     * @param code
     */
    public MyException(String msg, Integer code) {
        super(msg);
        this.code = code;
    }

    /**
     * 接收枚举类
     *
     * @param resultCodeEnum
     */
    public MyException(ResultCodeEnum resultCodeEnum) {
        super(resultCodeEnum.getMsg());
        this.code = resultCodeEnum.getCode();
    }

    @Override
    public String toString() {
        return "MyException{" +
                "code=" + code +
                ", message=" + this.getMessage() +
                '}';
    }
}

2. 自定义异常处理器

import com.qing.common.result.ResponseResult;
import com.qing.common.result.ResultCodeEnum;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 抛出503服务异常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ResponseResult error(Exception e) {
        System.out.println("Exception:" + e.getMessage());
        return ResponseResult.build(ResultCodeEnum.SERVICE_ERROR, null);
    }

    /**
     * 捕捉自定义异常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(MyException.class)
    @ResponseBody
    public ResponseResult error(MyException e) {
        System.out.println("Exception:" + e.getMessage());
        return ResponseResult.build(ResultCodeEnum.FAIL.getCode(), e.getMessage());
    }
}

三、JWT

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.0</version>
</dependency>

1. 实体类

User类:

@AllArgsConstructor
@NoArgsConstructor
@Data
@TableName("jwt_user")
@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
    @Id
    @TableField(value = "id")
    private Long id;
    @TableField(value = "user_name")
    private String userName;
    @TableField(value = "password")
    private String password;
    @TableField(value = "role")
    private String role;
}

JWTUser类:

这里需要创建一个JwtUser,用户存储用户的一些信息,也就是springsecurity认证时用到的UserDetails,所以这里实现了UserDetails

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Collections;
import java.util.Set;

@ToString
public class JwtUser implements UserDetails {
    @JsonIgnore
    private static final long serialVersionUID = 1L;

    public JwtUser() {
    }

    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 用户名
     */
    private String userName;

    /**
     * 密码
     */
    @JsonIgnore
    private String password;

    /**
     * 登录时间
     */
    private Long loginTime;

    /**
     * 登录用户
     */
    private User user;

    /**
     * 用户登录标识
     */
    private String token;

    /**
     * 过期时间
     */
    private Long expireTime;

    /**
     * 权限列表
     */
    private Set<String> permissions;

    /**
     * 角色列表
     */
    private Collection<? extends GrantedAuthority> authorities;

    public JwtUser(User user) {
        this.userId = user.getId();
        this.userName = user.getUserName();
        this.password = user.getPassword();
        this.user = user;
        permissions = null;
        // 这里只存储了一个角色的名字
        authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole()));
    }

    public User getUser() {
        return user;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public Long getLoginTime() {
        return loginTime;
    }

    public void setLoginTime(Long loginTime) {
        this.loginTime = loginTime;
    }

    public Long getExpireTime() {
        return expireTime;
    }

    public void setExpireTime(Long expireTime) {
        this.expireTime = expireTime;
    }

    public Set<String> getPermissions() {
        return permissions;
    }

    public void setPermissions(Set<String> permissions) {
        this.permissions = permissions;
    }

    @JsonIgnore
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @JsonIgnore
    @Override
    public String getPassword() {
        return this.password;
    }


    @JsonIgnore
    @Override
    public String getUsername() {
        return this.userName;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return true;
    }
}

2. Service层

UserDetailsServiceImpl:

这里实现UserDetailsService接口,重写了loadUserByUsername方法,因为springsecurity会自动帮我们验证所登录的用户、密码的正确性,也就是通过这个方法去查的数据库

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        User user = userMapper.findUserByUserName(s);
        return new JwtUser(user);
    }

}

因为是自己实现的方法,要调用userMapper:

@Mapper
public interface UserMapper extends BaseMapper<User> {
    User findUserByUserName(String userName);
}

UserMapper.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qing.mapper.UserMapper">
    <!--selectMapId-->
    <select id="findUserByUserName" parameterType="String" resultType="com.qing.pojo.User">
        select *
        from jwt_user
        where user_name = #{userName}
    </select>
</mapper>

3. 过滤器

认证过滤器JwtAuthenticationFilter

该过滤器是对用户登录时输入的账号密码进行验证,

可以追踪 authenticationManager.authenticate 源码中是调用了我们上面自定义的findUserByUserName方法去查数据库

/**
 * @author qingfan
 * @desc 登录验证过滤器 校验用户名密码是否正确
 * @date 2022/8/27 21:48
 */

public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        //自定义登录路径 localhost:8080/auth/login
        super.setFilterProcessesUrl("/auth/login");
    }

    @SneakyThrows
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {

        User loginUser = null;
        // 从输入流中获取到登录的信息
        try {
            loginUser = new ObjectMapper().readValue(request.getInputStream(), User.class);
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(loginUser.getUserName(), loginUser.getPassword(), new ArrayList<>())
            );
        } catch (Exception e) {
            System.out.println("Exception:" + e.getMessage());
            System.out.println("用户名或密码错误,loginUser:" + loginUser.toString());
            ResponseResult result = ResponseResult.fail(ResultCodeEnum.USERNAME_PWD_ERROR, null);
            HttpResponse.respBack(request, response, result);
        }
        return null;
    }

    // 成功验证后调用的方法
    // 如果验证成功,就生成token并返回
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {

        // 查看源代码会发现调用getPrincipal()方法会返回一个实现了`UserDetails`接口的对象
        // 所以就是JwtUser
        JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
        System.out.println("jwtUser:" + jwtUser.toString());
        String role = "";
        Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
        for (GrantedAuthority authority : authorities) {
            role = role + authority.getAuthority();
        }
        //使用用户id生成token,并且后续会用这个id作为redis主键
        String token = JwtUtil.createToken(jwtUser.getUserId(), role, false);
        // 返回创建成功的token
        // 但是这里创建的token只是单纯的token
        // 按照jwt的规定,最后请求的格式应该是 `Bearer token`
        response.setHeader("token", JwtUtil.TOKEN_PREFIX + token);
    }
}

鉴权过滤器JwtAuthorizationFilter

在执行一些需要认证的接口时(比如一些CRUD接口),会走到这个过滤器,会去获取request中的token,然后进行解析,判断token是否正确,登录态是否有效等

通过uid去redis查找对应的token,使用redis管理token

/**
 * @author qingfan
 * @desc 鉴权过滤器 校验是否有登录态
 * @date 2022/8/29 10:12
 */
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

    public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @SneakyThrows
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws IOException, ServletException {

        String tokenHeader = request.getHeader(JwtUtil.TOKEN_HEADER);
        // 如果请求头中没有Authorization信息(Cookie)则直接拦截
        if (tokenHeader == null || !tokenHeader.startsWith(JwtUtil.TOKEN_PREFIX)) {
            // chain.doFilter(request, response);
            System.out.println("tokenHeader:" + tokenHeader);
            ResponseResult<Object> result = ResponseResult.fail(ResultCodeEnum.TOKEN_ERROR, null);
            HttpResponse.respBack(null, response, result);
            return;
        }
        // 如果请求头中有token,则进行解析,并且设置认证信息
        try {
            SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader, response));
        } catch (MyException | SignatureException e) {
            System.out.println("Exception:" + e.getMessage());
            ResponseResult<Object> result = ResponseResult.fail(ResultCodeEnum.TOKEN_ERROR, null);
            HttpResponse.respBack(null, response, result);
            return;
        }
        super.doFilterInternal(request, response, chain);
    }

    // token验证解析
    private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader, HttpServletResponse response) throws IOException {
        String token = tokenHeader.replace(JwtUtil.TOKEN_PREFIX, "");
        //校验是否过期
        if (!JwtUtil.isExpiration(token)) {
            throw new MyException(ResultCodeEnum.TOKEN_ERROR);
        }
        //如果token未过期,那么就拿到里面的uid,查找redis,判断是否是该uid对应token
        //查找redis这里还没有实现
        String uid = JwtUtil.getUsername(token);
        String role = JwtUtil.getUserRole(token);
        if (uid != null) {
            System.out.println(new UsernamePasswordAuthenticationToken(uid, null, Collections.singleton(new SimpleGrantedAuthority(role))));
            //Collections.singleton(new SimpleGrantedAuthority(role))
            return new UsernamePasswordAuthenticationToken(uid, null, Collections.singleton(new SimpleGrantedAuthority(role)));
        }
        return null;
    }
}

4. springsecurity配置类

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    // 因为UserDetailsService的实现类实在太多啦,这里设置一下我们要注入的实现类
    @Qualifier("userDetailsServiceImpl")
    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()
                // 需要角色为ADMIN才能删除该资源
                //.antMatchers(HttpMethod.DELETE, "/tasks/**").hasAuthority("ROLE_ADMIN")
                // 其他都放行了
                .anyRequest().permitAll()
                .and()
                //认证拦截器
                .addFilter(new JwtAuthenticationFilter(authenticationManager()))
                //鉴权拦截器
                .addFilter(new JwtAuthorizationFilter(authenticationManager()))
                // 不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }
}

5. JWT工具类

jwt工具类,封装创建、校验token的方法
public class JwtUtil {
    //自定义token名字,我喜欢用Cookie
    public static final String TOKEN_HEADER = "Cookie";
    public static final String TOKEN_PREFIX = "Bearer ";

    private static final String SECRET = "jwtsecretdemo";
    private static final String ISS = "echisan";

    // 过期时间是3600秒,既是1个小时
    private static final long EXPIRATION = 3600L;

    // 选择了记住我之后的过期时间为7天
    private static final long EXPIRATION_REMEMBER = 604800L;

    // 添加角色的key
    private static final String ROLE_CLAIMS = "rol";

    // 创建token
    public static String createToken(Long uid, String role, boolean isRememberMe) {
        long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
        HashMap<String, Object> map = new HashMap<>();
        map.put(ROLE_CLAIMS, role);
        return Jwts.builder()
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .setClaims(map)
                .setIssuer(ISS)
                .setSubject(String.valueOf(uid))
                .setIssuedAt(new Date()) //签发时间
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))  //过期时间戳
                .compact();
    }

    // 从token中获取Subject
    public static String getUsername(String token) {
        return getTokenBody(token).getSubject();
    }

    //获取token的claims中的角色
    public static String getUserRole(String token) {
        return getTokenBody(token).get(ROLE_CLAIMS).toString();
    }

    // 是否已过期
    public static boolean isExpiration(String token) {
        try {
            //如果过期,执行getTokenBody方法时就会抛出异常
            getTokenBody(token);
        } catch (ExpiredJwtException e) {
            System.out.println("ExpiredJwtException:" + e.getMessage());
            return false;
        }
        return true;
    }

    private static Claims getTokenBody(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody();
    }

}

6. 角色权限控制

在对应的controller方法上添加@PreAuthorize注解

@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/{taskId}")
public String deleteTasks(@PathVariable("taskId") Integer id) {
  return "删除了id为:" + id + "的任务";
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
前后端分离的架构中,JWT令牌的通常涉及到端和后端之间的协作。以下是一种常见的前后端JWT令牌刷新的实现方式: 1. 在用户登录时,后端生成一个访问令牌(Access Token)和一个刷新令牌(Refresh Token)。访问令牌用于进行身份验证和授权,而刷新令牌用于后续的令牌刷新操作。 2. 后端将访问令牌和刷新令牌一起返回给前端,前端将它们存储在安全的地方(如HTTP Only Cookie或本地存储)。 3. 在每个请求中,前端将访问令牌作为身份验证凭证发送到后端。 4. 当访问令牌过期时,后端返回一个特定的HTTP响应状态码(例如401 Unauthorized)给前端。 5. 前端接收到过期响应后,使用存储的刷新令牌发送一个刷新令牌请求到后端。 6. 后端验证刷新令牌的有效性,并生成一个新的访问令牌返回给前端。 7. 前端接收到新的访问令牌后,更新存储的访问令牌,并使用新的访问令牌重新发送原始请求。 请注意,以上步骤中的具体实现方式会根据你使用的前端框架和后端技术而有所不同。以下是一些常见的实现细节: - 在前端,你需要实现一个拦截器或中间件,用于在每个请求中添加访问令牌,并处理过期响应。 - 在后端,你需要实现一个刷新令牌的API端点,用于接收刷新令牌请求并生成新的访问令牌。 - 在后端,你需要验证刷新令牌的有效性,通常是通过检查刷新令牌的签名和有效期等信息。 总结起来,前后端JWT令牌的刷新可以通过前端发送刷新令牌请求到后端,后端验证刷新令牌并生成新的访问令牌,最后前端使用新的访问令牌重新发送原始请求来实现。这样可以确保用户持续被授权且不需要重新登录

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值