@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class TokenEntity {
@Id
@GeneratedValue
private Long id;
private String token;
@Enumerated(EnumType.STRING)
private TokenType tokenType;
private boolean expired;
private boolean revoked;
@ManyToOne
@JoinColumn(name = "user_id")
private AppUserEntity user;
}
public interface TokenRepository extends JpaRepository<TokenEntity, Long> {
@Query("""
select t from TokenEntity t inner join AppUserEntity u on t.user.id = u.id
where u.id = :userId and (t.expired = false or t.revoked = false )
""")
List<TokenEntity> findAllValidTokensByUser(Long userId);
Optional<TokenEntity> findByToken(String token);
}
在註冊中添加兩個副程式
revokeAllUserTokens(user); 註銷先前所有的token
private void revokeAllUserTokens(AppUserEntity user){
var validUserTokens = tokenRepository.findAllValidTokensByUser(user.getId());
if (validUserTokens.isEmpty())
return;
validUserTokens.forEach(t -> {
t.setExpired(true);
t.setRevoked(true);
});
tokenRepository.saveAll(validUserTokens);
}
saveUserToken(user, jwtToken); 存取剛創建的Token
private void saveUserToken(AppUserEntity user, String jwtToken) {
var token = TokenEntity
.builder()
.user(user)
.token(jwtToken)
.tokenType(TokenType.BEARER)
.revoked(false)
.expired(false)
.build();
tokenRepository.save(token);
}
以上的動作是將多出來的token做註銷,讓每個使用者只能有1個token
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String userEmail;
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
try {
jwt = authHeader.substring(7);
userEmail = jwtService.extractUsername(jwt);
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
//檢查token是否有效
var isTokenValid = tokenRepository.findByToken(jwt)
.map(t -> !t.isExpired() && !t.isRevoked())
.orElseThrow();
//token有效則繼續
if (jwtService.isTokenValid(jwt, userDetails) && isTokenValid) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
return;
}
}
這裡我們加入isTokenValid 來判斷此token事是否是可用的。這麼一來我們每次請求時都會確定token的有效性
@Service
@RequiredArgsConstructor
public class LogoutService implements LogoutHandler {
private final TokenRepository tokenRepository;
@Override
public void logout(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) {
final String authHeader = request.getHeader("Authorization");
final String jwt;
// 以下條件為沒有攜帶Token的請求
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return;
}
jwt = authHeader.substring(7);
var storedToken = tokenRepository.findByToken(jwt).orElseThrow();
storedToken.setExpired(true);
storedToken.setRevoked(true);
tokenRepository.save(storedToken);
}
}
}
添加
.logout(logout -> logout
.logoutUrl("/apiauth/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext())
)
當你打出/api/auth/logout這支API時系統就會依據剛剛LogoutService去做登出的動作