# 實作 JWT [TOC] ###### tags: `spring` `security` --- ## DDL ```sql CREATE TABLE IF NOT EXISTS "jue_se" ( "id" serial2 PRIMARY KEY, "ming_cheng" varchar NOT NULL UNIQUE ); COMMENT ON TABLE"jue_se"IS'角色'; COMMENT ON COLUMN"jue_se"."id"IS'主键(序列)'; COMMENT ON COLUMN"jue_se"."ming_cheng"IS'角色名称'; CREATE TABLE IF NOT EXISTS "shen_fen" ( "wan_jia_id" int8 NOT NULL REFERENCES"wan_jia"("id") MATCH SIMPLE ON UPDATE CASCADE ON DELETE RESTRICT, "jue_se_id" int2 NOT NULL, REFERENCES"jue_se"("id") MATCH SIMPLE ON UPDATE CASCADE ON DELETE RESTRICT, PRIMARY KEY("wan_jia_id","jue_se_id") ); COMMENT ON TABLE"shen_fen"IS'身份'; COMMENT ON COLUMN"shen_fen"."wan_jia_id"IS'主键(雪花)'; COMMENT ON COLUMN"shen_fen"."jue_se_id"IS'主键(序列)'; ``` ## 專案結構 僅列出與實作 JWT 有關的檔案、目錄。 ``` ├─ src/ │ └─ main/ │ ├─ java/ │ │ └─ …/ │ │ ├─ WebSecurityConfigurer.java │ │ ├─ controller/ │ │ │ └─ WelcomeController.java │ │ ├─ entity/ │ │ │ ├─ Account.java │ │ │ └─ Role.java │ │ ├─ repository/ │ │ │ └─ AccountRepository.java │ │ └─ security/ │ │ ├─ AuthTokenFilter.java │ │ ├─ CSharpMd5PasswordEncoder.java │ │ ├─ JwtAuthenticationEntryPoint.java │ │ ├─ JwtResponse.java │ │ ├─ JwtUtils.java │ │ ├─ LoginRequest.java │ │ ├─ UserDetailsImpl.java │ │ └─ UserDetailsServiceImpl.java │ └─ resources/ │ └─ application.properties └─ pom.xml ``` ## `src/pom.xml` 在 `<dependencies/>` 裡加入以下 dependencies。 ```xml <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> ``` ## ER-Diagram ## `src/…/entity/User.java`、`src/…/entity/Role.java` 基本的 Many-To-Many,不在本文範疇,故僅呈現重點;`User` 為 owner side。 ```java @JoinTable( inverseJoinColumns = { @JoinColumn( name = "jue_se_id", referencedColumnName = "id", nullable = false ) }, joinColumns = { @JoinColumn( name = "wan_jia_id", referencedColumnName = "id", nullable = false ) }, name = "shen_fen" ) @ManyToMany private Set<Role> roles; ``` `Role` 為 the other side。 ```java @ManyToMany(mappedBy = "roles") private Set<Account> accounts; ``` ### Data Access Objects ```java @Repository public interface AccountRepository extends JpaRepository<Account, Long> { /** * @param login 帐号名称 * @return 帐号 */ public Optional<Account> findOneByLoginIgnoreCase(final String login); } ``` ## `src/…/security/UserDetailsImpl.java` ```java @EqualsAndHashCode @Getter public class UserDetailsImpl implements UserDetails { private static final Logger LOGGER = LoggerFactory.getLogger(JwtUtils.class); private static final long serialVersionUID = -7784938525705246555L; /** * 主键 */ @SuppressWarnings("FieldMayBeFinal") private Long id; /** * 密码 */ @SuppressWarnings("FieldMayBeFinal") private String password; /** * 帐号名称 */ @SuppressWarnings("FieldMayBeFinal") private String username; /** * 授权 */ @SuppressWarnings("FieldMayBeFinal") private Collection<? extends GrantedAuthority> authorities; /** * @param account 玩家 * @return 用户核心信息 */ public static UserDetailsImpl convert(Account account) { List<GrantedAuthority> authorities = account. getRoles(). stream(). map( role -> new SimpleGrantedAuthority( role.getName() ) ). collect( Collectors.toList() ); return new UserDetailsImpl( account.getId(), account.getShadow(), account.getLogin(), authorities ); } /** * @param id 主键(雪花) * @param password 密码 * @param username 帐号名称 * @param authorities 授权 */ public UserDetailsImpl(Long id, String password, String username, Collection<? extends GrantedAuthority> authorities) { this.id = id; this.password = password; this.username = username; this.authorities = authorities; } @Override public String toString() { try { return new JsonMapper().writeValueAsString(this); } catch (JsonProcessingException ignore) { return "null"; } } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } } ``` ## `src/…/security/UserDetailsServiceImpl.java` ```java @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private AccountRepository accountRepository; /** * @param username 帐号名称 * @return 根据帐号名称查找用户 */ @Override public UserDetails loadUserByUsername(String username) { return UserDetailsImpl.convert( accountRepository. findOneByLoginIgnoreCase(username). orElseThrow( () -> new UsernameNotFoundException( String.format( "User Not Found with username: %s", username ) ) ) ); } } ``` ## `src/…/security/JwtUtils.java` ```java package ….security; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureException; import io.jsonwebtoken.UnsupportedJwtException; import java.util.Date; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @Component public class JwtUtils { private static final Logger LOGGER = LoggerFactory.getLogger(JwtUtils.class); @Value("${io.jsonwebtoken.jjwt.jwtSecret}") private String jwtSecret; @Value("${io.jsonwebtoken.jjwt.jwtExpiration}") private int jwtExpiration; public String generateJwtToken(Authentication authentication) { UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal(); return Jwts. builder(). setSubject(userPrincipal.getUsername()). setIssuedAt(new Date()). setExpiration( new Date( new Date(System.currentTimeMillis()).getTime() + jwtExpiration ) ). signWith( SignatureAlgorithm.HS512, jwtSecret ). compact(); } public String getUserNameFromJwtToken(String token) { return Jwts. parser(). setSigningKey(jwtSecret). parseClaimsJws(token).getBody(). getSubject(); } public boolean validateJwtToken(String token) { try { Jwts. parser(). setSigningKey(jwtSecret). parseClaimsJws(token); return true; } catch (SignatureException signatureException) { LOGGER.error( "Invalid JWT signature: {}", signatureException.getMessage() ); } catch (MalformedJwtException malformedJwtException) { LOGGER.error( "Invalid JWT token: {}", malformedJwtException.getMessage() ); } catch (ExpiredJwtException expiredJwtException) { LOGGER.error( "JWT token is expired: {}", expiredJwtException.getMessage() ); } catch (UnsupportedJwtException unsupportedJwtException) { LOGGER.error( "JWT token is unsupported: {}", unsupportedJwtException.getMessage() ); } catch (IllegalArgumentException illegalArgumentException) { LOGGER.error( "JWT claims string is empty: {}", illegalArgumentException.getMessage() ); } return false; } } ``` ## `src/…/security/AuthTokenFilter.java` ```java package ….security; import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; public class AuthTokenFilter extends OncePerRequestFilter { private static final Logger LOGGER = LoggerFactory.getLogger(AuthTokenFilter.class); @Autowired private JwtUtils jwtUtils; @Autowired private UserDetailsServiceImpl userDetailsService; private String parseJwt(HttpServletRequest request) { String authorizationHeader = request.getHeader("Authorization"); if (StringUtils.hasText(authorizationHeader) && authorizationHeader.startsWith("Bearer ")) { return authorizationHeader.substring( 7, authorizationHeader.length() ); } return null; } @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { try { String jwt = parseJwt(request); if (jwt != null && jwtUtils.validateJwtToken(jwt)) { String username = jwtUtils.getUserNameFromJwtToken(jwt); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authentication.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception exception) { LOGGER.error( "Cannot set user authentication: {}", exception ); } filterChain.doFilter(request, response); } } ``` ## `src/…/security/JwtAuthenticationEntryPoint.java` ```java package ….security; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class); @Override public void commence( HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException ) throws IOException, ServletException { LOGGER.error( "由于某种原因而无效与身份验证对象相关的异常:{}", authenticationException.getMessage() ); response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "由于某种原因而无效与身份验证对象相关的异常💢" ); } } ``` ## `src/main/resources/application.properties` ``` io.jsonwebtoken.jjwt.jwtExpiration=86400000 io.jsonwebtoken.jjwt.jwtSecret=… ``` ## `src/…/security/JwtAuthenticationEntryPoint.java` ```java package ….security; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Formatter; import org.apache.logging.log4j.util.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.crypto.password.PasswordEncoder; public class CSharpMd5PasswordEncoder implements PasswordEncoder { private static final Logger LOGGER = LoggerFactory.getLogger(CSharpMd5PasswordEncoder.class); @Override public String encode(CharSequence rawPassword) { try { MessageDigest messageDigest = MessageDigest.getInstance("MD5"); messageDigest.update( rawPassword.toString().getBytes( StandardCharsets.ISO_8859_1 ) ); Formatter formatter = new Formatter(); for (final byte b : messageDigest.digest()) { formatter.format( "%02x", b ); } return formatter.toString().toUpperCase(); } catch (NoSuchAlgorithmException noSuchAlgorithmException) { return Strings.EMPTY; } } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { if (rawPassword == null) { throw new IllegalArgumentException("rawPassword cannot be null"); } if (encodedPassword == null || encodedPassword.length() == 0) { LOGGER.warn("Empty encoded password"); return false; } return MessageDigest.isEqual( encode(rawPassword). getBytes(StandardCharsets.ISO_8859_1), encodedPassword. getBytes(StandardCharsets.ISO_8859_1) ); } } ``` ## `src/…/WebSecurityConfigurer.java` ```java package …; import ….security.JwtAuthenticationEntryPoint; import ….AuthTokenFilter; import ….UserDetailsServiceImpl; import ….CSharpMd5PasswordEncoder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity( jsr250Enabled = true, prePostEnabled = true, securedEnabled = true ) public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { private static final Logger LOGGER = LoggerFactory.getLogger(WebSecurityConfigurer.class); @Autowired private JwtAuthenticationEntryPoint authenticationEntryPointJwt; @Autowired private UserDetailsServiceImpl userDetailsService; @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity. // 添加要使用的 CorsFilter cors(). and(). // 停用 CSRF 保护 csrf().disable(). exceptionHandling(). authenticationEntryPoint(authenticationEntryPointJwt). and(). // 配置会话管理 sessionManagement(). sessionCreationPolicy(SessionCreationPolicy.STATELESS). and(). authorizeRequests(). antMatchers( "/logIn", "/signUp" ). permitAll(). anyRequest(). authenticated(); httpSecurity.addFilterBefore( authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class ); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder. userDetailsService(userDetailsService). passwordEncoder(passwordEncoder()); } @Bean public AuthTokenFilter authenticationJwtTokenFilter() { return new AuthTokenFilter(); } @Bean public PasswordEncoder passwordEncoder() { return new CSharpMd5PasswordEncoder(); } } ``` ## `src/…/security/LoginRequest.java` ```java package ….security; import javax.validation.constraints.NotBlank; import lombok.Getter; import lombok.Setter; @Getter @Setter public class LoginRequest { @NotBlank private String username; @NotBlank private String password; } ``` ## `src/…/security/JwtResponse.java` ```java package ….security; import java.util.List; import lombok.Getter; import lombok.Setter; @Getter @Setter public class JwtResponse { private String accessToken; private String type = "Bearer"; private Long id; private String username; private List<String> roles; public JwtResponse(String accessToken, Long id, String username, List<String> roles) { this.accessToken = accessToken; this.id = id; this.username = username; this.roles = roles; } } ``` ## `src/…/controller/WelcomeController.java` ```java package ….controller; import ….security.JwtResponse; import ….security.JwtUtils; import ….security.LoginRequest; import ….security.UserDetailsImpl; import java.util.List; import java.util.stream.Collectors; import javax.validation.Valid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/") public class WelcomeController { private static final Logger LOGGER = LoggerFactory.getLogger(WelcomeController.class); @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtUtils jwtUtils; @GetMapping("/") ResponseEntity<?> index() { return ResponseEntity.ok(); } @PostMapping("/logIn") ResponseEntity<?> logIn(@RequestBody @Valid LoginRequest loginRequest) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getUsername(), loginRequest.getPassword() ) ); SecurityContextHolder. getContext(). setAuthentication(authentication); String jwtToken = jwtUtils.generateJwtToken(authentication); UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); List<String> roles = userDetails. getAuthorities(). stream(). map( item -> item.getAuthority() ). collect( Collectors.toList() ); return ResponseEntity.ok( new JwtResponse( jwtToken, userDetails.getId(), userDetails.getUsername(), roles ) ); } } ```