Java — Spring Boot Todo List Built From Scratch (5)
使用者登入後將會產生一組 Token 並返回,該 Token 會被存放在 Redis,並有相應的 TTL,而取得使用者資訊則是可以用 Token 換取當前使用者的資訊
Redis
在開始功能之前,先把 Redis Repo 建立起來,先前在第一篇已完成對 Redis 的連線,現在則是要實作對 Redis 操作的 Repository
RedisConfig
RedisTemplate
是 Spring 提供用於與 Redis 交互的工具類,提供常見資料結構的操作函式,主要依靠此類操作 Redis
該類本身是泛型,根據需求初始化成自己需要的類型,除了 <String,String>
,也可以宣告成 <String,Object>
儲存 JSON 格式的值
// RedisConfig.java
@Configuration
public class RedisConfig {
/**
* Redis工具類
* RedisTemplate<String,String> 表示儲存的KV都是Stirng
*/
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory conn) {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(conn);
// 將Key序列化為String
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 將Value序列化為String
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
}
RedisRepo
使用上方設定的 Redis 工具類函式包裝成 RedisRepo 給其他組件使用,目前還沒有特別需求,就常見的先寫
@Repository
public class RedisRepo {
private final RedisTemplate<String, String> redisTemplate;
@Autowired
public RedisRepo(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void save(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
public void save(String key, String value, Duration time) {
redisTemplate.opsForValue().set(key, value, time);
}
public String get(String key) {
return redisTemplate.opsForValue().get(key);
}
public void del(String key) {
redisTemplate.delete(key);
}
}
額外紀錄一下 RedisTemplate 提供的函式
- 存儲和獲取單個值:
opsForValue()
方法提供了對字符串值的存儲和獲取操作,如set(key, value)
和get(key)
。 - 存儲和獲取哈希:
opsForHash()
方法提供了對哈希數據結構的存儲和獲取操作,如hset(key, hashKey, value)
和hget(key, hashKey)
。 - 存儲和獲取列表:
opsForList()
方法提供了對列表數據結構的存儲和獲取操作,如leftPush(key, value)
和range(key, start, end)
。 - 存儲和獲取集合:
opsForSet()
方法提供了對集合數據結構的存儲和獲取操作,如add(key, value)
和members(key)
。 - 存儲和獲取有序集合:
opsForZSet()
方法提供了對有序集合數據結構的存儲和獲取操作,如add(key, value, score)
和rangeByScore(key, min, max)
。
使用者登入
UserRepo
根據JPA的用法,如果有自定義的欄位要搜尋的話,依照 findByxxxAndxxx
就可以自動產生相對應的功能,欄位名稱與類型需與 Model 相同,並使用大駝峰
// UserRepo.java
@Repository
public interface UserRepo extends CrudRepository<User, Long> {
public User findByAccountAndPwdHash(String account, String pwdHash);
}
UserService
此類的當前的注入情況,加上 final
確保不會再被修改,同時使用 final
的話,注入就需要改成使用建構函式
@Service
public class UserService {
private final UserRepo repo;
private final RedisRepo redisRepo;
private final JwtUtil jwtUtil;
@Autowired
public UserService(UserRepo repo, RedisRepo redisRepo, JwtUtil jwtUtil) {
this.repo = repo;
this.redisRepo = redisRepo;
this.jwtUtil = jwtUtil;
}
//...
}
將 Request 的帳號與密碼 Hash 後,與帳號一同在資料庫搜尋
// UserService.java
/**
* 登入
*/
public User UserLogin(String account, String password) throws Exception {
String pwdhash = Hash.generateHash(account, password);
User user = repo.findByAccountAndPwdHash(account, pwdhash);
if (user == null) {
throw new UserException().NotFoundUser(new IllegalArgumentException());
}
return user;
}
使用 JwtUtil
產生 token 後存入 Redis
/**
* 產生token
*/
public String GenerateToken(User user) {
// 產生 Token
String token = jwtUtil.generateToken(user);
// 存到Redis
redisRepo.save(user.getId().toString(), token, Duration.ofDays(jwtUtil.expDay));
return token;
}
UserController
組合流程!
/**
* 使用者登入
*/
@PostMapping("/login")
public Response UserLogin(@Valid @RequestBody UserLoginDto data, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return new Response().Error().ErrorMessage(bindingResult.getAllErrors());
}
try {
// 使用者登入
User user = userSvc.UserLogin(data.account, data.password);
// 產生 Token
String token = userSvc.GenerateToken(user);
Map<String, String> res = new HashMap<>();
res.put("token", token);
return new Response().AddData(res);
} catch (Exception e) {
return new Response().Error().ErrorMessage(e);
}
}
使用者資訊
利用 Token 取得當前 Reuqest 的用戶,在前篇中的 JwtAuthFilter
已完成將 Request 中的 Token 驗證與放入 SecurityContextHolder
中,所以只需要使用已經處理好的資料即可
JwtAuthFilter
因為多使用了 Redis,所以 checkToken
流程要多加一個檢查該 Token 是否有在 Redis 中
// JwtAuthFilter.java
/**
* 檢查Token是否有在Redis中
*/
public void isExistInRedis(String token) throws Exception {
// 取出token中的使用者編號
Long id = extractID(token);
// 查詢redis
String tokenInRedis = redisRepo.get(id.toString());
// 比對token是否相同
if (!tokenInRedis.equals(token)) {
throw new IllegalAccessError();
}
}
// JwtAuthFilter.java
/**
* 驗證 Token
*/
public boolean checkToken(String token) throws Exception {
try {
// 傳入加密後的JWT Token
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
// 檢查到期日
isExpired(token);
// 檢查是否有在Redis中
isExistInRedis(token);
return true;
} catch (Exception e) {
throw new AuthException().AuthFail(e);
}
}
JwtAuthToken
這個類原先有使用 JwtUtil,但 JwtUtil 作為 Component ,應該是注入給各組件使用,不是在各類自己初始化使用,這樣同樣的對象在系統中會存在很多,而不是共享對象
而 JwtAuthToken 不是 Component,只在需要時調用,對其注入 JwtUtil 不太合理,所以調整了實作函式,不再調用 JwtUtil 取得 Token 內的使用者編號,而是在初始化時就傳入使用者編號
// JwtAuthToken.java
public class JwtAuthToken extends AbstractAuthenticationToken {
private String token;
private Long principal;
public JwtAuthToken(String token, Long id) {
super(null);
this.token = token;
this.principal = id;
}
/**
* 驗證資訊
*/
@Override
public Object getCredentials() {
return token;
}
/**
* 身份資訊,使用者編號
*/
@Override
public Long getPrincipal() {
return this.principal;
}
}
UserService
利用 Token 內的 ID 查詢使用者資訊
// UserService.java
/**
* 取得使用者資訊
*/
public User GetUserInfoById(Long id) throws Exception {
// orElse()用來設置當findById()找不到時的返回結果
// findById()的Optional<T>類是用於處理對象可能為null容器類
User user = repo.findById(id).orElse(null);
if (user == null) {
throw new UserException().NotFoundUser(new IllegalArgumentException());
}
return user;
}
UserController
從 SecurityContextHolder
取得 Authentication
,此為我自定義 JwtAuthToken
類,所以調用 getPrincipal()
時會回傳類型為 Long 的 id,再調用 UserService
取得使用者資訊
/**
* 取得使用者資訊
*/
@GetMapping("/info")
public Response UserInfo() {
try {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Long userId = (Long) auth.getPrincipal();
User user = userSvc.GetUserInfoById(userId);
return new Response().AddData(user);
} catch (Exception e) {
return new Response().Error().ErrorMessage(e);
}
}
寫一個下午的進度,要多花幾個下午才能整理成文章 (汗)