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