iT邦幫忙

2023 iThome 鐵人賽

DAY 22
0
Mobile Development

Spring Boot & Android Studio教學系列 第 22

掌握Spring Security:Token歷史紀錄與登出(EP5)

  • 分享至 

  • xImage
  •  

創建Token實體類來檢測歷史紀錄

TokenEntity

@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;

}

TokenRepository

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

在過濾器中添加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的有效性

登出(註銷當前使用token)

LogoutService

@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);
        }
    }
}

接著到SecurityConfiguration去設定Logout

添加

.logout(logout -> logout
        .logoutUrl("/apiauth/logout")
        .addLogoutHandler(logoutHandler)
        .logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext())
        )

當你打出/api/auth/logout這支API時系統就會依據剛剛LogoutService去做登出的動作


上一篇
掌握Spring Security:登入註冊API測試(EP4)
下一篇
使用 Spring Boot 建置 Firebase 雲端訊息傳遞 (FCM)
系列文
Spring Boot & Android Studio教學30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言