---
# System prepended metadata

title: 實作 JWT
tags: [spring, security]

---

# 實作 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
			)
		);
	}
}
```
