SpringBoot整合JWT 三

功能简介

  • 前后端分离
  • 跨域访问
  • 登录拦截判断

代码地址

SpringBoot整合Jwt实现前后端分离代码:下载地址一
SpringBoot整合Jwt实现前后端分离代码:下载地址二

后端代码

数据库信息

 CREATE TABLE `tb_user` (
  `id` int(45) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1002 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;

第一步:创建Maven项目

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-loggingartifactId>
dependency>

<dependency>
    <groupId>org.projectlombokgroupId>
    <artifactId>lombokartifactId>
    <version>1.18.10version>
dependency>
<dependency>
    <groupId>com.alibabagroupId>
    <artifactId>fastjsonartifactId>
    <version>1.2.62version>
dependency>

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starterartifactId>
dependency>

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
dependency>

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-testartifactId>
    <scope>testscope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintagegroupId>
            <artifactId>junit-vintage-engineartifactId>
        exclusion>
    exclusions>
dependency>

<dependency>
    <groupId>io.jsonwebtokengroupId>
    <artifactId>jjwtartifactId>
    <version>0.9.1version>
dependency>

<dependency>
    <groupId>commons-codecgroupId>
    <artifactId>commons-codecartifactId>
    <version>1.13version>
dependency>
<dependency>
    <groupId>com.auth0groupId>
    <artifactId>java-jwtartifactId>
    <version>3.10.2version>
dependency>

<dependency>
    <groupId>mysqlgroupId>
    <artifactId>mysql-connector-javaartifactId>
    <version>8.0.18version>
dependency>
<dependency>
    <groupId>com.alibabagroupId>
    <artifactId>druidartifactId>
    <version>1.1.22version>
dependency>
<dependency>
    <groupId>org.mybatisgroupId>
    <artifactId>mybatisartifactId>
    <version>3.5.4version>
dependency>
<dependency>
    <groupId>org.mybatis.spring.bootgroupId>
    <artifactId>mybatis-spring-boot-starterartifactId>
    <version>2.1.2version>
dependency>
<dependency>
    <groupId>org.mybatisgroupId>
    <artifactId>mybatis-springartifactId>
    <version>2.0.4version>
dependency>
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
    <groupId>org.mybatis.spring.bootgroupId>
    <artifactId>mybatis-spring-boot-autoconfigureartifactId>
    <version>2.1.2version>
dependency>


<dependency>
    <groupId>com.github.pagehelpergroupId>
    <artifactId>pagehelper-spring-boot-starterartifactId>
    <version>1.2.5version>
dependency>

<dependency>
    <groupId>org.springframeworkgroupId>
    <artifactId>spring-txartifactId>
    <version>5.2.3.RELEASEversion>
dependency>

第二步:配置文件

application.yml

server:
  port: 80
  servlet:
    context-path: /jd

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/db_test?useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: root
mybatis:
  config-location: classpath:mybatis.xml
  #Mapper.xml所在的位置
  mapper-locations: classpath*:mapper/*.xml
  #Entity扫描的model包
  type-aliases-package: com.hc.bean

logging:
  config: classpath:logback.xml

mybatis.xml



<configuration>

    <settings>
        <setting name="logImpl" value="SLF4J"/>
        
        <setting name="mapUnderscoreToCamelCase" value="true" />
    settings>

configuration>

logback.xml


<configuration scan="true" scanPeriod="60 seconds" debug="false">
    
    <jmxConfigurator />
    
    
    <property name="log_dir" value="./logs" />
    
    <property name="maxHistory" value="30" />
    
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>
                
                %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger - %msg%n
            pattern>
        encoder>
    appender>
    
    <root>
        
        <level value="info" />
        
        <appender-ref ref="console" />
    root>
configuration>

第三步:工具类

Base64Util.java

@Slf4j
public class Base64Util {
    private static final String charset = "utf-8";

    // 解密
    public static String decode(String data) {
        try {
            if (null == data) {
                return null;
            }
            return new String(Base64.decodeBase64(data.getBytes(charset)), charset);
        } catch (UnsupportedEncodingException e) {
            log.error(String.format("字符串:%s,解密异常", data), e);
        }
        return null;
    }

    // 加密
    public static String encode(String data) {
        try {
            if (null == data) {
                return null;
            }
            return new String(Base64.encodeBase64(data.getBytes(charset)), charset);
        } catch (UnsupportedEncodingException e) {
            log.error(String.format("字符串:%s,加密异常", data), e);
        }
        return null;
    }

}

JwtConst.java

public interface JwtConst {
    // 代表这个JWT的接收对象
    String clientId = "098f6bcd4621d373cade4e832627b4f6";
    // 密钥, 经过Base64加密, 可自行替换
    String secret = "MDk4ZjZiY2Q0NjIxZD";
    // JWT的签发主体,存入issuer
    String name = "restapiuser";
    // 过期时间,单位毫秒
    long expires = 60000;
    // 从客户端传递过来的token的名称
    String token = "token";
}

JwtTokenUtil.java

@Slf4j
public class JwtTokenUtil {

    // 解析jwt
    public static Claims parseJWT(String jsonWebToken, String base64Security) {
        try {
            Claims claims = Jwts.parser()
                   .setSigningKey(DatatypeConverter.parseBase64Binary(base64Security))
                   .parseClaimsJws(jsonWebToken).getBody();
            return claims;
        } catch (ExpiredJwtException  eje) {
            log.error("===== Token过期 =====", eje);
            throw new CustomException(ResultCode.PERMISSION_TOKEN_EXPIRED);
        } catch (Exception e){
            log.error("===== token解析异常 =====", e);
            throw new CustomException(ResultCode.PERMISSION_TOKEN_INVALID);
        }
    }

    // 构建jwt
    public static String createJWT(String userId, String username, String role) {
        try {
            // 使用HS256加密算法
            SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

            long nowMillis = System.currentTimeMillis();
            Date now = new Date(nowMillis);

            //生成签名密钥
            byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(JwtConst.secret);
            Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

            //userId是重要信息,进行加密下
            String encryUserId = Base64Util.encode(userId);

            //添加构成JWT的参数
            JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
                    // 可以将基本不重要的对象信息放到claims
                    .claim("role", role)
                    .claim("userId", encryUserId)
                    .setSubject(username)           // 代表这个JWT的主体,即它的所有人
                    .setIssuer(JwtConst.clientId)              // 代表这个JWT的签发主体
                    .setIssuedAt(new Date())       // 是一个时间戳,代表这个JWT的签发时间
                    .setAudience(JwtConst.name)          // 代表这个JWT的接收对象
                    .signWith(signatureAlgorithm, signingKey);
            //添加Token过期时间
            long TTLMillis = JwtConst.expires;
            if (TTLMillis >= 0) {
                long expMillis = nowMillis + TTLMillis;
                Date exp = new Date(expMillis);
                builder.setExpiration(exp)  // 是一个时间戳,代表这个JWT的过期时间;
                        .setNotBefore(now); // 是一个时间戳,代表这个JWT生效的开始时间 
            }
            //生成JWT
            return builder.compact();
        } catch (Exception e) {
            log.error("签名失败", e);
            throw new CustomException(ResultCode.PERMISSION_SIGNATURE_ERROR);
        }
    }

    // 从token中获取用户名
    public static String getUsername(String token){
        return parseJWT(token, JwtConst.secret).getSubject();
    }

    // 从token中获取用户ID
    public static String getUserId(String token){
        String encryUserId = parseJWT(token, JwtConst.secret).get("userId").toString();
        return Base64Util.decode(encryUserId);
    }

    // 是否已过期
    public static boolean isExpiration(String token) {
        return parseJWT(token, JwtConst.secret).getExpiration().before(new Date());
    }

}

第四步:通用类

Result.java

@Data
@ToString
public class Result<T> {
    private Integer code;
    private String msg;
    private T data;


    public Result(String msg) {
        this.msg = msg;
    }

    public Result(ResultCode resultCode) {
        this.code = resultCode.getCode();
        this.msg = resultCode.getMsg();
    }

    public Result(ResultCode resultCode, T data) {
        this(resultCode);
        this.data = data;
    }
    
    public static Result SUCCESS() {
        return new Result(ResultCode.SUCCESS);
    }

    public static <T> Result SUCCESS(T data) {
        return new Result(ResultCode.SUCCESS, data);
    }

    public static Result FAIL() {
        return new Result(ResultCode.FAIL);
    }

    public static Result FAIL(String msg) {
        return new Result(msg);
    }

}

ResultCode.java

public enum ResultCode {

    /* 成功状态码 */
    SUCCESS(0,"操作成功!"),

    /* 错误状态码 */
    FAIL(-1,"操作失败!"),

    /* 参数错误:10001-19999 */
    PARAM_IS_INVALID(10001, "参数无效"),

    /* 用户错误:20001-29999*/
    USER_NOT_LOGGED_IN(20001, "用户未登录,请先登录"),

    /* 系统错误:40001-49999 */
    SYSTEM_INNER_ERROR(40001, "系统繁忙,请稍后重试"),


    /* 权限错误:70001-79999 */
    PERMISSION_TOKEN_EXPIRED(70004, "token已过期"),
    PERMISSION_TOKEN_INVALID(70006, "无效token"),
    PERMISSION_SIGNATURE_ERROR(70007, "签名失败");

    //操作代码
    int code;
    //提示信息
    String msg;

    ResultCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

CustomException.java

public class CustomException extends RuntimeException {
    //错误代码
    ResultCode resultCode;

    public CustomException(ResultCode resultCode){
        super(resultCode.getMsg());
        this.resultCode = resultCode;
    }

    public CustomException(ResultCode resultCode, Object... args){
        super(resultCode.getMsg());
        String message = MessageFormat.format(resultCode.getMsg(), args);
        resultCode.setMsg(message);
        this.resultCode = resultCode;
    }

    public ResultCode getResultCode(){
        return resultCode;
    }
}

GlobalExceptionHandler.java

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    // 处理自定义异常
    @ExceptionHandler(CustomException.class)
    public Result handleException(CustomException e) {
        // 打印异常信息
        log.error("### 异常信息:{} ###", e.getMessage());
        return new Result(e.getResultCode());
    }

    // 参数错误异常
    @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
    public Result handleException(Exception e) {
        if (e instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException validException = (MethodArgumentNotValidException) e;
            BindingResult result = validException.getBindingResult();
            StringBuffer errorMsg = new StringBuffer();
            if (result.hasErrors()) {
                List<ObjectError> errors = result.getAllErrors();
                errors.forEach(p ->{
                    FieldError fieldError = (FieldError) p;
                    errorMsg.append(fieldError.getDefaultMessage()).append(",");
                    log.error("### 请求参数错误:{"+fieldError.getObjectName()+"},field{"+fieldError.getField()+ "},errorMessage{"+fieldError.getDefaultMessage()+"}"); });
            }
        } else if (e instanceof BindException) {
            BindException bindException = (BindException)e;
            if (bindException.hasErrors()) {
                log.error("### 请求参数错误: {}", bindException.getAllErrors());
            }
        }
        return new Result(ResultCode.PARAM_IS_INVALID);
    }

    // 处理所有不可知的异常
    @ExceptionHandler(Exception.class)
    public Result handleOtherException(Exception e){
        //打印异常堆栈信息
        e.printStackTrace();
        // 打印异常信息
        log.error("### 不可知的异常:{} ###", e.getMessage());
        return new Result(ResultCode.SYSTEM_INNER_ERROR);
    }
}

第五步:自定义注解

Logintoken.java

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginToken {
    boolean required() default true;
}

Passtoken.java

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
    boolean required() default true;
}

第六步:自定义拦截器并注册

JwtInterceptor.java

@Slf4j
public class JwtInterceptor implements HandlerInterceptor {
    @Autowired
    UserService userService;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception {
        // 如果不是映射到方法直接通过
        if (!(object instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) object;
        Method method = handlerMethod.getMethod();

        //检查是否有passtoken注释,有则跳过认证
        if (method.isAnnotationPresent(PassToken.class)) {
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
                log.info("当前所访问的资源不需要登录");
                return true;
            }
        }
        //检查有没有需要用户权限的注解
        if (method.isAnnotationPresent(LoginToken.class)) {
            LoginToken loginToken = method.getAnnotation(LoginToken.class);
            if (loginToken.required()) {
                // 从 http 请求头中取出 token
                String authHeaderKey = JwtConst.token;
                String token = request.getHeader(authHeaderKey);
                if (token == null || token.length() == 0) {
                    token = request.getParameter(authHeaderKey);
                }
                if (token == null || token.length() == 0) {
                    log.info("用户未登录,请先登录");
                    throw new CustomException(ResultCode.USER_NOT_LOGGED_IN);
                }
                String userId = JwtTokenUtil.getUserId(token);

                User user = userService.selectByPrimaryKey(Integer.parseInt(userId));
                if (user == null) {
                    log.info("用户不存在,请重新登录");
                    throw new RuntimeException("用户不存在,请重新登录");
                }
                log.info("用户存在,登录成功");
                return true;
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
    }
}

InterceptorConfig.java

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {//跨域请求
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**");    //用于设置拦截器的过滤路径规则
// 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
    }
    @Bean
    public JwtInterceptor authenticationInterceptor() {
        return new JwtInterceptor();
    }
}

第七步:MyBatis操作相关

实体类User.jva

@Getter
@Setter
@ToString
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    private static final long serialVersionUID = -8390887042859558153L;
    private Integer id;
    private String username;
    private String password;
}

UserDao.java
@Mapper
public interface UserMapper {
    User selectByPrimaryKey(Integer id);

    List<User> selectByUsername(@Param("username")String username);
}

UserMapper.xml



<mapper namespace="com.hc.mapper.UserMapper">
    <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultType="User">
        select
        id, username, `password`
        from db_test.tb_user
        where id = #{id}
    select>
    <select id="selectByUsername" resultType="User">
        select
        id, username, `password`
        from db_test.tb_user
        where username=#{username}
    select>
mapper>

UserService.java

public interface UserService {
    User selectByPrimaryKey(Integer id);
    List<User> selectByUsername(String username);
}

UserServiceImp.java

@Service
public class UserServiceImpl implements UserService{
    @Resource
    private UserMapper userMapper;
    @Override
    public User selectByPrimaryKey(Integer id) {
        return userMapper.selectByPrimaryKey(id);
    }
    @Override
    public List<User> selectByUsername(String username) {
        return userMapper.selectByUsername(username);
    }
}

第八步:Controller

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

    @Autowired
    private UserService userService;

    //登录
    @RequestMapping("/login")
    public Result login(HttpServletResponse response, String username, String password) {
        List<User> userList = userService.selectByUsername(username);
        if (userList == null || userList.size() == 0) {
            log.info("登录失败,用户不存在");
            return Result.FAIL("登录失败,用户不存在");
        }
        String role = "admin";
        for (User user : userList) {
            if (user.getPassword().equals(password)) {
                log.info("登录成功");
                String token = JwtTokenUtil.createJWT(user.getId()+"",username,role);
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("token",token);
                return Result.SUCCESS(jsonObject);
            }
        }
        log.info("登录失败,密码错误");
        return Result.FAIL("登录失败,密码错误");
    }

    @LoginToken
    @RequestMapping("/getUserById")
    public User getUserById(HttpServletResponse response,Integer userId) {
        log.info("访问需要登录才能访问的受保护的资源");
        User user = userService.selectByPrimaryKey(userId);
        return user;
    }

    // 不加注解的话默认不验证,登录接口一般是不验证的。
    // getMessage()加上了登录注解,说明该接口必须登录获取token后,在请求头中加上token并通过验证才可以访问
    @PassToken
    @GetMapping("/getMsg")
    public String getMsg() {
        log.info("访问不需要登录就能访问的资源");
        return "不需要通过验证";
    }
}

前端代码

创建项目,然后将jquery-2.2.4.min.js库拷贝到项目的根路径下的js目录中。项目结构如下图所示:
SpringBoot整合JWT 三_第1张图片

login.html


<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面title>
    <script type="text/javascript" src="js/jquery-2.2.4.min.js">script>
head>
<body>
    <form>
        <input type="text" name="username" id="username" value="zhangsan"> <br>
        <input type="text" name="password" id="password" value="123456"><br>
        <input type="button" id="login" value="登录"/>
    form>
    <div id="show">div>

    <script type="text/javascript">
        $("#login").click(function () {
            $.ajax({
                url: "http://localhost:80/jd/user/login",
                type: "POST",
                data: {
                    username: $("#username").val(),
                    password: $("#password").val()
                },
                dataType: "json",
                success: function (res) {
                    localStorage.setItem("token",res.data.token);
                    window.location.href="user.html";
                },
                error:function () {
                    $("#show").html("error");
                }
            });
        });
    script>
body>
html>

user.html


<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>用户信息页面title>
    <script type="text/javascript" src="js/jquery-2.2.4.min.js">script>
head>
<body>
    <a href="http://localhost:80/jd/user/getMsg">请求公共信息a>
    <div id="emp">div>

    <script type="text/javascript">
        $.ajax({
            url: "http://localhost:80/jd/user/getUserById",
            type: "POST",
            data: {
                userId: 1001,
                token:localStorage.getItem("token")
            },
            dataType: "json",
            success:function (res) {
                $("#emp").text(JSON.stringify(res));
            },
            error:function () {
                console.log("error");
            }
        });
    script>
body>
html>

测试

在login.html页面中请求后台,然后在打开的user.html页面中请求公共信息,在控制台中看到如下信息:
在这里插入图片描述

你可能感兴趣的:(#,SpringBoot)