SpringBoot結(jié)合JWT實現(xiàn)用戶登錄、注冊、鑒權(quán)
用戶登錄、注冊及鑒權(quán)是我們基本所有系統(tǒng)必備的,也是很核心重要的一塊,這一塊的安全性等都比較重要,實現(xiàn)的方案其實也有幾種,從以前的
cookie
+session
的方案,到現(xiàn)在常用的jwt
的方案,這篇文章就講講目前在公司中最常用的jwt
方案如何實現(xiàn)。
一、用戶注冊與登錄
完成用戶注冊與登錄有個核心點就是密碼的加密與驗證,我們目前比較常用的方案是密碼+鹽
再采用MD5加密
的方案,
鹽的方式一般可以在application.yml
里面寫死,但安全性相對較差,還有就是通過UUID
生成存到數(shù)據(jù)庫里,這里我們采用第二種安全性更高的方式。
sql
如下:
DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, `salt` varchar(255) NOT NULL, `admin` int(1) DEFAULT '0', `age` int(3) NOT NULL, `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, `deleted` int(1) DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
對應(yīng)的User
實體類
domian.entity.User
:
import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import java.util.Date; @Data @AllArgsConstructor @NoArgsConstructor @Accessors(chain = true) @TableName("user") public class User { @TableId private Long id; private String username; private String password; private String salt; private Boolean admin; private Integer age; @TableField(fill = FieldFill.INSERT) private Date createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private Date updateTime; private Integer deleted; }
這里我們使用了Mybatis Plus
的邏輯刪除及自動填充功能,不太清楚的可以看看我的文章SpringBoot 整合 Mybatis Plus 實現(xiàn)基本CRUD功能
接收用戶注冊信息的DTO
domain.dto.registryUserDto
:
import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.UUID; @Data @AllArgsConstructor @NoArgsConstructor public class registryUserDto { private String username; private String password; @JsonIgnore private String salt = UUID.randomUUID().toString().replaceAll("-", ""); private Boolean admin; private Integer age; }
@JsonIgnore
為忽略前端的傳值,這里使用我們UUID
生成的值。
用戶登錄的DTO
domain.dto.LoginUserDto
:
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class LoginUserDto { private String username; private String password; }
用戶注冊與登錄的controller
:
controller.UserController
:
import com.jk.domain.dto.registryUserDto; import com.jk.domain.dto.LoginUserDto; import com.jk.service.UserService; import com.jk.domain.vo.ResponseResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @PostMapping("/registry") public ResponseResult registryUser(@RequestBody registryUserDto registryUserDto) { return userService.registryUser(registryUserDto); } @PostMapping("/login") public ResponseResult login(@RequestBody LoginUserDto loginUserDto) { return userService.login(loginUserDto); } }
用戶注冊與登錄的service
:
service.UserService
:
import com.jk.domain.dto.registryUserDto; import com.jk.domain.dto.LoginUserDto; import com.jk.domain.vo.ResponseResult; public interface UserService { ResponseResult registryUser(registryUserDto registryUserDto); ResponseResult login(LoginUserDto loginUserDto); }
用戶注冊與登錄的service實現(xiàn)類
:
service.impl.UserServiceImpl
:
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.jk.domain.dto.registryUserDto; import com.jk.domain.dto.LoginUserDto; import com.jk.domain.entity.User; import com.jk.enums.AppHttpCodeEnum; import com.jk.mapper.UserMapper; import com.jk.service.UserService; import com.jk.domain.vo.ResponseResult; import com.jk.utils.BeanCopyUtils; import com.jk.utils.JwtUtils; import com.jk.utils.RedisCache; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.DigestUtils; import java.util.concurrent.TimeUnit; @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Autowired private RedisCache redisCache; @Override public ResponseResult registryUser(registryUserDto registryUserDto) { String password = registryUserDto.getPassword(); String salt = registryUserDto.getSalt(); String md5Password = DigestUtils.md5DigestAsHex((password + salt).getBytes()); registryUserDto.setPassword(md5Password); User user = BeanCopyUtils.copyBean(registryUserDto, User.class); userMapper.insert(user); return ResponseResult.okResult(); } @Override public ResponseResult login(LoginUserDto loginUserDto) { String username = loginUserDto.getUsername(); String password = loginUserDto.getPassword(); LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUsername, username); User user = userMapper.selectOne(queryWrapper); String md5Password = DigestUtils.md5DigestAsHex((password + user.getSalt()).getBytes()); if (!md5Password.equals(user.getPassword())) { return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_ERROR); } String token = JwtUtils.createToken(user.getId()); redisCache.setCacheObject("TOKEN_" + token, JSON.toJSONString(user), 1, TimeUnit.DAYS); return ResponseResult.okResult(token); } }
用戶注冊時,我們把密碼+salt
進(jìn)行MD5加密
,然后入庫,用戶登錄時,根據(jù)username
查出用戶,再把用戶傳入的密碼+salt
進(jìn)行MD5加密
與數(shù)據(jù)庫查出的用戶進(jìn)行密碼比較判斷是否驗證通過。這里還有使用到一個JWT工具類
,驗證通過后使用JWT工具類
生成token
和用戶信息存到redis
里面,這里需要引入下fastjson
來對用戶信息字符串化存,然后返回前端token
。
具體JWT
使用如下:
- 首先引入
fastjson
和jwt
的依賴包
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.26</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
JWT工具類
的封裝
utils.JwtUtils
:
import io.jsonwebtoken.Jwt; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; import java.util.HashMap; import java.util.Map; public class JwtUtils { private static final String jwtToken = "1234567890p[]l;'"; public static String createToken(Long userId) { Map<String, Object> claims = new HashMap<>(); claims.put("userId", userId); JwtBuilder jwtBuilder = Jwts.builder() // 設(shè)置有效載荷 .setClaims(claims) // 設(shè)置簽發(fā)時間 .setIssuedAt(new Date()) // 設(shè)置過期時間 .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000)) // 采用HS256方式簽名,key就是用來簽名的秘鑰 .signWith(SignatureAlgorithm.HS256, jwtToken); String token = jwtBuilder.compact(); return token; } public static Map<String, Object> checkToken(String token) { try { Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token); return (Map<String, Object>) parse.getBody(); } catch (Exception e) { e.printStackTrace(); return null; } } }
到此我們已經(jīng)完成了用戶的注冊和登錄功能。但還有一個問題就是用戶鑒權(quán),我們在調(diào)用其他接口時如何判斷用戶是否已登錄。
二、用戶鑒權(quán)
用戶鑒權(quán)我們需要用到ThreadLocal
來存儲用戶信息,我們首先創(chuàng)建這個工具類
utils.UserThreadLocal
:
import com.jk.domain.entity.User; public class UserThreadLocal { private UserThreadLocal() { } private static final ThreadLocal<User> LOCAL = new ThreadLocal<>(); public static void put(User user) { LOCAL.set(user); } public static User get() { return LOCAL.get(); } public static void remove() { LOCAL.remove(); } }
還需要在service
中實現(xiàn)驗證token的邏輯
service.UserService
:
User checkToken(String token);
service.impl.UserServiceImpl
:
@Override public User checkToken(String token) { if (StringUtils.isEmpty(token)) { return null; } Map<String, Object> map = JwtUtils.checkToken(token); if (map == null) { return null; } String userJson = redisCache.getCacheObject("TOKEN_" + token); if (StringUtils.isEmpty(userJson)) { return null; } User user = JSON.parseObject(userJson, User.class); return user; }
使用攔截器實現(xiàn)token驗證
handler.interceptor.LoginInterceptor
:
import com.jk.domain.entity.User; import com.jk.enums.AppHttpCodeEnum; import com.jk.exception.SystemException; import com.jk.service.UserService; import com.jk.utils.UserThreadLocal; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Component @Slf4j public class LoginInterceptor implements HandlerInterceptor { @Autowired private UserService userService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } String token = request.getHeader("token"); log.info("===============request start==============="); log.info("request uri:{}", request.getRequestURI()); log.info("request method:{}", request.getMethod()); log.info("token:{}", token); log.info("===============request end==============="); if (StringUtils.isEmpty(token)) { throw new SystemException(AppHttpCodeEnum.NEED_LOGIN); } User user = userService.checkToken(token); if (user == null) { throw new SystemException(AppHttpCodeEnum.NEED_LOGIN); } UserThreadLocal.put(user); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserThreadLocal.remove(); } }
配置WebMvcConfigurer
使用登錄攔截器
import com.jk.handler.interceptor.LoginInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/web/**") .addPathPatterns("/admin/**"); } }
會對/web
及/admin
的所有接口做登錄驗證,這個大家根據(jù)自己項目需求調(diào)整。
到此這篇關(guān)于SpringBoot結(jié)合JWT實現(xiàn)用戶登錄、注冊、鑒權(quán)的文章就介紹到這了,更多相關(guān)SpringBoot JWT用戶登錄、注冊、鑒權(quán)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
項目連接nacos配置中心報錯:Client not connected, current
這篇文章主要介紹了項目連接nacos配置中心報錯:Client not connected, current status:STARTING的解決方案,采用了mysql作為持久化的數(shù)據(jù)庫,docker作為運行的環(huán)境,感興趣的朋友跟隨小編一起看看吧2024-03-03SpringBoot入坑筆記之spring-boot-starter-web 配置文件的使用
本篇向小伙伴介紹springboot配置文件的配置,已經(jīng)全局配置參數(shù)如何使用的。需要的朋友跟隨腳本之家小編一起學(xué)習(xí)吧2018-01-01Spring Boot + Mybatis-Plus實現(xiàn)多數(shù)據(jù)源的方法
這篇文章主要介紹了Spring Boot + Mybatis-Plus實現(xiàn)多數(shù)據(jù)源的方法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11Spark操作之a(chǎn)ggregate、aggregateByKey詳解
這篇文章主要介紹了Spark操作之a(chǎn)ggregate、aggregateByKey詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06