目錄
登錄接口説明:LoginController
一、獲取圖形驗證碼接口
controller層:
service層:
二、登錄接口
controller層:
service層:
mapper層:
三、JwtUtil創建
四、獲取登陸用户個人信息接口
controller層:
(重要)因為我們解析token的方法是在攔截器裏調用,所以如果我們每一次獲取登錄者的時候,要獲取token進行解析,連續調用兩次parseToken()方法就不太合理了。所以為避免重複解析,通常會在攔截器將Token解析完畢後,將結果保存至ThreadLocal中,這樣一來,我們便可以在整個請求的處理流程中進行訪問了。
修改攔截器:
service層:
登錄接口説明:LoginController
@Tag(name = "後台管理系統登錄管理")
@RestController
@RequestMapping("/admin")
public class LoginController {
@Autowired
private LoginService service;
}
一、獲取圖形驗證碼接口
接口名稱:getCaptcha
請求方式:Get
請求路徑:/admin/login/captcha
請求參數:無
返回類型:CaptchaVo
我們先查看CaptchaVo有哪些屬性:
@Data
@Schema(description = "圖像驗證碼")
@AllArgsConstructor
public class CaptchaVo {
@Schema(description="驗證碼圖片信息")
private String image;
@Schema(description="驗證碼key")
private String key;
}
需要獲取驗證碼圖片的信息和存儲在redis裏的key。那麼怎麼生成驗證碼呢?我們需要使用開源的驗證碼生成工具EasyCaptcha。
導入maven依賴:
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
</dependency>
controller層:
@Operation(summary = "獲取圖形驗證碼")
@GetMapping("login/captcha")
public Result<CaptchaVo> getCaptcha() {
CaptchaVo result= service.getCaptcha();
return Result.ok(result);
}
service層:
public CaptchaVo getCaptcha() {
SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 4); //分別設置驗證碼區域的長、寬、以及驗證碼長度。
String code = specCaptcha.text().toLowerCase(); //把驗證碼轉成小寫
String key = RedisConstant.ADMIN_LOGIN_PREFIX + UUID.randomUUID(); //遵循命名規範
stringRedisTemplate.opsForValue().set(key,code,RedisConstant.ADMIN_LOGIN_CAPTCHA_TTL_SEC, TimeUnit.SECONDS); //把key和驗證碼值存入redis
return new CaptchaVo(specCaptcha.toBase64(),key);
}
本項目Reids中的key需遵循以下命名規範:項目名:功能模塊名:其他,例如
admin:login:123456
二、登錄接口
接口名稱:login
請求方式:Post
請求路徑:/admin/login
請求參數:@RequestBody LoginVo loginVo
返回類型:Result<String>
controller層:
@Operation(summary = "登錄")
@PostMapping("login")
public Result<String> login(@RequestBody LoginVo loginVo) {
String jwt=service.login(loginVo);
return Result.ok(jwt);
}
loginVo用來封裝前端登錄界面用户提交的用户名、密碼、驗證碼以及驗證碼在redis的key。
@Data
@Schema(description = "後台管理系統登錄信息")
public class LoginVo {
@Schema(description="用户名")
private String username;
@Schema(description="密碼")
private String password;
@Schema(description="驗證碼key")
private String captchaKey;
@Schema(description="驗證碼code")
private String captchaCode;
}
service層:
public String login(LoginVo loginVo) {
if (loginVo.getCaptchaCode()==null){ //判斷輸入的驗證碼是否為空
throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_NOT_FOUND);
}
String code = stringRedisTemplate.opsForValue().get(loginVo.getCaptchaKey()); //從redis中獲取code
if (code==null){ //判斷查詢的code是否為空
throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_EXPIRED);
}
if (!code.equals(loginVo.getCaptchaCode().toLowerCase())){ //如果code與輸入的code的小寫相等
throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_ERROR);
}
SystemUser systemUser = systemUserMapper.selectOneByUsername(loginVo.getUsername()); //通過用户名查詢用户
if (systemUser==null){
throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_NOT_EXIST_ERROR);
}
if (systemUser.getStatus()== BaseStatus.DISABLE){
throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_DISABLED_ERROR);
}
if (!systemUser.getPassword().equals(DigestUtils.md5Hex(loginVo.getPassword()))){
throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_ERROR);
}
return JwtUtil.createToken(systemUser.getId(),systemUser.getUsername()); //返回token
}
mapper層:
自定義通過用户名字查詢用户的方法。
<select id="selectOneByUsername" resultType="com.atguigu.lease.model.entity.SystemUser">
select id,
username,
password,
name,
type,
phone,
avatar_url,
additional_info,
post_id,
status
from system_user
where is_deleted = 0
and username = #{username}
</select>
三、JwtUtil創建
JwtUtil是用來生成token來識別登錄的用户是否合法,如果合法則從數據庫中查詢用户信息,並響應給前端。而且後續前端想要請求後端接口也需要解析攜帶的token。
導入依賴:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
public class JwtUtil {
private static long tokenExpiration = 60 * 60 * 1000L;
private static SecretKey tokenSignKey = Keys.hmacShaKeyFor("M0PKKI6pYGVWWfDZw90a0lTpGYX1d4AQ".getBytes());
/**
* 創建token方法
* @param userId
* @param username
* @return
*/
public static String createToken(Long userId, String username) {
String token = Jwts.builder().
setSubject("LOGIN_USER").
setExpiration(new Date(System.currentTimeMillis() + 3600000)).
claim("userId", userId).
claim("username", username).
signWith(tokenSignKey, SignatureAlgorithm.HS256).
compact();
return token;
}
/**
* 解析token方法
* @param token
* @return
*/
public static Claims parseToken(String token){
if (token==null){
throw new LeaseException(ResultCodeEnum.ADMIN_LOGIN_AUTH);
}
try {
JwtParser jwtParser = Jwts.parser().setSigningKey(tokenSignKey)
.build();
Jws<Claims> claimsJws = jwtParser.parseClaimsJws(token);
Claims claims = claimsJws.getBody();
return claims;
}catch (ExpiredJwtException e){
throw new LeaseException(ResultCodeEnum.TOKEN_EXPIRED);
}catch (JwtException e){
throw new LeaseException(ResultCodeEnum.TOKEN_INVALID);
}
}
四、獲取登陸用户個人信息接口
接口名稱:info
請求方式:Get
請求路徑:/admin/info
請求參數:無
返回類型:Result<SystemUserInfoVo>
controller層:
返回的類型為VO對象,需要自己定義查詢方法。
(重要)因為我們解析token的方法是在攔截器裏調用,所以如果我們每一次獲取登錄者的時候,要獲取token進行解析,連續調用兩次parseToken()方法就不太合理了。所以為避免重複解析,通常會在攔截器將Token解析完畢後,將結果保存至ThreadLocal中,這樣一來,我們便可以在整個請求的處理流程中進行訪問了。
ThreadLocal的主要作用是為每個使用它的線程提供一個獨立的變量副本,使每個線程都可以操作自己的變量,而不會互相干擾,其用法如下圖所示。
每個ThreadLocal對象都擁有set、get、remove方法,我們只需要編寫邏輯調用即可自定義存儲、獲取、清理本地線程對象的方法。
public class LoginUserHolder {
public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>(); //創建本地線程對象
public static void setLoginUser(LoginUser loginUser) {
threadLocal.set(loginUser); //把登錄用户信息存儲進本地線程對象
}
public static LoginUser getLoginUser() {
return threadLocal.get(); //獲取登錄用户信息
}
public static void clear() {
threadLocal.remove(); //清理存儲的用户信息
}
}
@Operation(summary = "獲取登錄用户個人信息")
@GetMapping("info")
public Result<SystemUserInfoVo> info() {
Long userId = LoginUserHolder.getLoginUser().getUserId();
SystemUserInfoVo systemUserInfoVo=service.getLoginUserInfoById(userId);
return Result.ok(systemUserInfoVo);
}
修改攔截器:
@Component
public class AuthenticationIntercepter implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("access-token");
Claims claims = JwtUtil.parseToken(token);
Long userId = claims.get("userId", Long.class);
String username = claims.get("username", String.class);
LoginUserHolder.setLoginUser(new LoginUser(userId, username)); //封裝登錄對象存儲進本地線程對象
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
LoginUserHolder.clear(); //清理本地線程方法
}
}
service層:
public SystemUserInfoVo getLoginUserInfoById(Long userId) {
SystemUser systemUser = systemUserMapper.selectById(userId);
SystemUserInfoVo systemUserInfoVo = new SystemUserInfoVo();
systemUserInfoVo.setAvatarUrl(systemUser.getAvatarUrl());
systemUserInfoVo.setName(systemUser.getName());
return systemUserInfoVo;
}