功能简介
- 前后端分离
- 跨域访问
- 登录拦截判断
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项目
org.springframework.boot
spring-boot-starter-logging
org.projectlombok
lombok
1.18.10
com.alibaba
fastjson
1.2.62
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
io.jsonwebtoken
jjwt
0.9.1
commons-codec
commons-codec
1.13
com.auth0
java-jwt
3.10.2
mysql
mysql-connector-java
8.0.18
com.alibaba
druid
1.1.22
org.mybatis
mybatis
3.5.4
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.2
org.mybatis
mybatis-spring
2.0.4
org.springframework.boot
spring-boot-starter-jdbc
org.mybatis.spring.boot
mybatis-spring-boot-autoconfigure
2.1.2
com.github.pagehelper
pagehelper-spring-boot-starter
1.2.5
org.springframework
spring-tx
5.2.3.RELEASE
第二步:配置文件
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
logback.xml
%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger - %msg%n
第三步:工具类
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 {
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 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 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 selectByUsername(@Param("username")String username);
}
UserMapper.xml
select
id, username, `password`
from db_test.tb_user
where id = #{id}
select
id, username, `password`
from db_test.tb_user
where username=#{username}
UserService.java
public interface UserService {
User selectByPrimaryKey(Integer id);
List 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 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 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目录中。项目结构如下图所示:
登录页面
$("#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");
}
});
});
user.html
用户信息页面
请求公共信息
$.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");
}
});
测试
在login.html页面中请求后台,然后在打开的user.html页面中请求公共信息,在控制台中看到如下信息: