原文鏈接: https://www.callicoder.com/spring-boot-spring-security-jwt-mysql-react-app-part-2/
歡迎來(lái)到全棧開發(fā)系列第二章(Spring Boot厚骗,Spring Security个少,JWT坝疼,MySQL牙言,React)琢锋。
在第一章,我們創(chuàng)建了基礎(chǔ)的領(lǐng)域模型和數(shù)據(jù)倉(cāng)庫(kù)并啟動(dòng)了項(xiàng)目。
在本文中,我們將通過結(jié)合Spring Security和JWT來(lái)配置認(rèn)證功能皱碘,編寫用戶注冊(cè),登錄的API隐孽。
本項(xiàng)目的完整源碼托管在Github癌椿,如果你碰到困難,可隨時(shí)參考菱阵。
spring-boot-spring-security-jwt-login-signup-apis.jpg
安全機(jī)制概述
- 構(gòu)建一個(gè)新用戶注冊(cè)的API踢俄,信息有 name, username, email , password
- 構(gòu)建一個(gè)讓用戶通過 username或email和password登錄的API。在驗(yàn)證完用戶憑證后晴及,API應(yīng)該生成并返回一個(gè)JWT身份授權(quán)令牌
客戶端請(qǐng)求受保護(hù)資源時(shí)應(yīng)該在每個(gè)request的頭部在Authorization
中放入JWT Token都办。 - 配置Spring Security限制受保護(hù)資源的訪問。比如
- 登錄虑稼,注冊(cè)接口以及其他的靜態(tài)資源(圖片琳钉,scripts,css)應(yīng)該被通過
- 創(chuàng)建調(diào)查蛛倦,發(fā)起投票等接口應(yīng)只能被認(rèn)證通過的用戶訪問歌懒。
- 配置Spring Security,如果用戶未攜帶JWT Token訪問受保護(hù)資源溯壶,則拋出401未認(rèn)證通過錯(cuò)誤
- 配置基于
角色
的授權(quán)及皂。比如- 只有角色是
ADMIN
的用戶可以創(chuàng)建一個(gè)調(diào)查 - 只有角色是
USER
的用戶可以投票
- 只有角色是
配置 Spring Security 和 JWT
下面的類是實(shí)現(xiàn)安全的重中之重甫男,他包含了幾乎所有本項(xiàng)目需要的安全相關(guān)的配置。
讓我們首先在com.example.polls.config
包下創(chuàng)建SecurityConfig
验烧,然后我們照著代碼學(xué)習(xí)每個(gè)配置到底做了什么事查剖。
package com.example.polls.config;
import com.example.polls.security.CustomUserDetailsService;
import com.example.polls.security.JwtAuthenticationEntryPoint;
import com.example.polls.security.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
securedEnabled = true,
jsr250Enabled = true,
prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CustomUserDetailsService customUserDetailsService;
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean(BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/",
"/favicon.ico",
"/**/*.png",
"/**/*.gif",
"/**/*.svg",
"/**/*.jpg",
"/**/*.html",
"/**/*.css",
"/**/*.js")
.permitAll()
.antMatchers("/api/auth/**")
.permitAll()
.antMatchers("/api/user/checkUsernameAvailability", "/api/user/checkEmailAvailability")
.permitAll()
.antMatchers(HttpMethod.GET, "/api/polls/**", "/api/users/**")
.permitAll()
.anyRequest()
.authenticated();
// Add our custom JWT security filter
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
以上的 SecurityConfig
類在你的IDE中會(huì)有幾處編譯錯(cuò)誤,因?yàn)槲覀冞€沒有定義此類中需要用到的一些類噪窘。我們將在文章的后文中定義他們笋庄。
但在此之前,讓我們理解這些注解的意義和代碼中相關(guān)的配置的含義倔监。
1. @EnableWebSecurity
這是Spring Sercurity主要的注解直砂,用于在項(xiàng)目中開啟Web Security。
2. @EnableGlobalMethodSecurity
這個(gè)注解用于開啟方法級(jí)別的安全浩习,你可使用以下3個(gè)類型的注解去保護(hù)你的方法静暂。
-
securityEnable: 它的作用是使
@Secured
注解可以保護(hù)你的Controller/Service層方法。
@Secured("ROLE_ADMIN")
public User getAllUsers() {}
@Secured({"ROLE_USER", "ROLE_ADMIN"})
public User getUser(Long id) {}
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public boolean isUsernameAvailable() {}
-
jsr250Enabled:它可以讓
@RolesAllowed
可以像這樣使用
@RolesAllowed("ROLE_ADMIN")
public Poll createPoll() {}
- ** prePostEnabled:**它可以使用@PreAuthorize和@PostAuthorize注解谱秽,基于表達(dá)式構(gòu)造更復(fù)雜的訪問控制語(yǔ)法
@PreAuthorize("isAnonymous()")
public boolean isUsernameAvailable() {}
@PreAuthorize("hasRole('USER')")
public Poll createPoll() {}
3. WebSecurityConfigurerAdapter
此類實(shí)現(xiàn)了Spring Security的 WebSecurityConfigurer
接口霉赡。它提供了默認(rèn)的安全配置項(xiàng),如需自定義一些自己的需求可以通過繼承他覆蓋他的方法來(lái)更改供搀。
我們的Security
類繼承了WebSecurityConfigurerAdapter
并復(fù)寫了他的幾個(gè)方法來(lái)提供自己的安全配置甸陌。
4. CustomUserDetailsService
為驗(yàn)證用戶和實(shí)現(xiàn)各種基于角色的檢查,Spring Security需要我們提供用戶的一些信息近哟。
因此驮审,他存在一個(gè)名為UserDetailService
的接口,內(nèi)容為通過username檢索返回User相關(guān)信息吉执。
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
我們定義了CustomUserDetailsService
實(shí)現(xiàn)了UserDetailsService
接口并且提供了一個(gè)loadUserByUsername
的具體實(shí)現(xiàn)疯淫。
注意,loadUserByUsername
方法返回一個(gè)UserDetails
對(duì)象戳玫,而Spring Security正需要他用于進(jìn)行各種認(rèn)證與基于角色的驗(yàn)證熙掺。
在我們的實(shí)現(xiàn)中,我們還定義了一個(gè)定制的UserPrincipal
類實(shí)現(xiàn)了UserDetails接口咕宿,在loadUserByUsername()
方法中我們返回了UserPrincipal
對(duì)象币绩。(loadUserByUsername()
方法返回的是UserDetails
對(duì)象,作者是因?yàn)?code>UserPrincipal實(shí)現(xiàn)了UserDetails
接口荠列,所以這么說也沒問題)
5. JwtAuthenticationEntryPoint
當(dāng)客戶端想訪問受保護(hù)資源类浪,缺沒有提供合適的認(rèn)證令牌時(shí),這個(gè)類返回401未認(rèn)證通過錯(cuò)誤給客戶端肌似。這個(gè)類實(shí)現(xiàn)了Spring Security的AuthenticationEntryPoint
接口费就。
6. JwtAuthenticationFilter
我們用JwtAuthenticationFilter
實(shí)現(xiàn)了過濾器的功能
- 從requests中Header里的
Authorization
中讀取JWT授權(quán)信息 - 驗(yàn)證token
- 用戶信息與token關(guān)聯(lián)
- 把用戶信息放到Spring Security的上下文(
SecurityContext
)中,Spring Security使用用戶信息去做一些校驗(yàn)川队。我們也可以從SecurityContext
中取出用戶信息用在自己的業(yè)務(wù)邏輯中力细。
7.AuthenticationManagerBuilder 和 AuthenticationManager
AuthenticationManagerBuilder
用于創(chuàng)建AuthenticationManager
實(shí)例睬澡,這個(gè)接口就是Spring Security用于認(rèn)證用戶的主要接口。
你可以使用AuthenticationManagerBuilder
構(gòu)建 基于內(nèi)存的認(rèn)證眠蚂,LDAP認(rèn)證煞聪,JDBC 認(rèn)證,或自定義認(rèn)證逝慧。
在我們的例子中昔脯,我們提供了自己的customUserDetailsService
和passwordEncoder
去構(gòu)建AuthenticationManager
。
我們后面將會(huì)使用配置好的AuthenticationManager在登錄接口中驗(yàn)證用戶笛臣。
8.HttpSecurity configurations
HttpSecurity configurations
用于配置安全功能云稚,像 csrf
, sessionManagement
,也可以通過多種條件配置規(guī)則來(lái)保護(hù)資源沈堡。
在我們的例子中静陈,我們給予靜態(tài)資源和一些任意用戶可訪問的API公開權(quán)限,同時(shí)也限制了一些API只能被登錄的用戶所訪問诞丽。
我們當(dāng)然也可以把JWTAuthenticationEntryPoint
和自定義的JWTAuthenticationFilter
配置到HttpSecurity
中鲸拥。
創(chuàng)建自定義Spring Security類,F(xiàn)ilter僧免,Annotations
在上一節(jié)刑赶,我們將許多自定義的類和Filters與Spring Security結(jié)合,在這一節(jié)猬膨,我們將逐一定義這些類角撞。
以下所創(chuàng)建的所有類都在包com.example.poll.security
中呛伴。
1.自定義Spring Security AuthenticationEntryPoint
我們第一個(gè)定義的與Spring Security相關(guān)的類是 JwtAuthenticationEntryPoint
勃痴。他實(shí)現(xiàn)了AuthenticationEntryPoint
接口并實(shí)現(xiàn)了接口中的commence()
方法。當(dāng)一個(gè)未經(jīng)認(rèn)證過的用戶嘗試訪問一個(gè)需要認(rèn)證才可以訪問的資源時(shí)热康,這個(gè)方法將被調(diào)用沛申。
在本例中,我們簡(jiǎn)化response僅包含401錯(cuò)誤碼和一些異常信息姐军。
package com.example.polls.security;
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;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
logger.error("Responding with unauthorized error. Message - {}", e.getMessage());
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
}
}
2. 自定義Spring Security UserDetails
下一步铁材,讓我們自定義UserPrincipal
類實(shí)現(xiàn)UserDetails
類。此類是UserDetails
類的實(shí)現(xiàn)奕锌,在我們自定義的UserDetailService作為結(jié)果返回著觉。Spring Security會(huì)使用存儲(chǔ)在UserPrincipal
對(duì)象的數(shù)據(jù)來(lái)進(jìn)行認(rèn)證和授權(quán)。
以下就是完整的UserPrincipal
類代碼 -
package com.example.polls.security;
import com.example.polls.model.User;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class UserPrincipal implements UserDetails {
private Long id;
private String name;
private String username;
@JsonIgnore
private String email;
@JsonIgnore
private String password;
private Collection<? extends GrantedAuthority> authorities;
public UserPrincipal(Long id, String name, String username, String email, String password, Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.name = name;
this.username = username;
this.email = email;
this.password = password;
this.authorities = authorities;
}
public static UserPrincipal create(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream().map(role ->
new SimpleGrantedAuthority(role.getName().name())
).collect(Collectors.toList());
return new UserPrincipal(
user.getId(),
user.getName(),
user.getUsername(),
user.getEmail(),
user.getPassword(),
authorities
);
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
@Override
public String getUsername() {
return username;
}
@Override
public String getPassword() {
return password;
}
@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;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserPrincipal that = (UserPrincipal) o;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
3. 自定義Spring Security UserDetailService
現(xiàn)在我們來(lái)完成自定義的UserDetailService
惊暴,可以通過username
查找到User
信息饼丘。
package com.example.polls.security;
import com.example.polls.model.User;
import com.example.polls.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String usernameOrEmail)
throws UsernameNotFoundException {
// Let people login with either username or email
User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
.orElseThrow(() ->
new UsernameNotFoundException("User not found with username or email : " + usernameOrEmail)
);
return UserPrincipal.create(user);
}
// This method is used by JWTAuthenticationFilter
@Transactional
public UserDetails loadUserById(Long id) {
User user = userRepository.findById(id).orElseThrow(
() -> new UsernameNotFoundException("User not found with id : " + id)
);
return UserPrincipal.create(user);
}
}
第一個(gè)方法loadUserByUsername()
是為Spring Security提供的,注意findByUsernameOrEmail
方法辽话,他是可以用username
或email
登錄的肄鸽。
第二個(gè)方法loadUserById()
是為JWTAuthenticationFilter
提供的卫病,我們稍后定義它。
4. 生成和驗(yàn)證Token的實(shí)用類
下面這個(gè)類的作用在用戶登錄成功后生成JWT典徘,驗(yàn)證請(qǐng)求中頭部的JWT授權(quán)信息蟀苛。
package com.example.polls.security;
import io.jsonwebtoken.*;
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;
import java.util.Date;
@Component
public class JwtTokenProvider {
private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
@Value("${app.jwtSecret}")
private String jwtSecret;
@Value("${app.jwtExpirationInMs}")
private int jwtExpirationInMs;
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public Long getUserIdFromJWT(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException ex) {
logger.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
logger.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
logger.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
logger.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
logger.error("JWT claims string is empty.");
}
return false;
}
}
上面這個(gè)類用到了@Value
去properties
中讀取JWT secret
和expiration time
。
讓我們?cè)谂渲梦募屑由线@2個(gè)屬性的配置吧 -
JWT Properties
## App Properties
app.jwtSecret= JWTSuperSecretKey
app.jwtExpirationInMs = 604800000
5. 定義Spring Security AuthenticationFilter
最后逮诲,我們創(chuàng)建JWTAuthenticationFilter
來(lái)從request中拿到JWT token并驗(yàn)證它帜平,建立token與用戶之間的聯(lián)系,并在Spring Security中放行梅鹦。
package com.example.polls.security;
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;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private CustomUserDetailsService customUserDetailsService;
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromJWT(jwt);
UserDetails userDetails = customUserDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
在上面這個(gè)過濾器里罕模,我們首先從請(qǐng)求頭的Authorization
中拿到JWT解析,拿到了用戶ID帘瞭。其后淑掌,我們從數(shù)據(jù)庫(kù)中拿到了用戶具體信息并將驗(yàn)證信息放到了Spring Security的上下文中。
注意蝶念,在filter中通過查詢數(shù)據(jù)庫(kù)拿到用戶信息是可選的抛腕。你也可以將用戶的賬號(hào),密碼及角色信息編碼后放到JWT claims中媒殉,然后通過解析JWT的claims去創(chuàng)建一個(gè)UserDetails
担敌。如此便不會(huì)產(chǎn)生數(shù)據(jù)庫(kù)IO。
然而廷蓉,從數(shù)據(jù)庫(kù)中讀取用戶信息還是非常有用的全封。比如,當(dāng)用戶角色更改后或用戶在創(chuàng)建JWT后更改了密碼桃犬,則不應(yīng)讓他還用之前的JWT登錄刹悴。
6. 獲取當(dāng)前登錄用戶的自定義注解
Spring Security提供了一個(gè)叫@AuthenticationPrincipal
的注解去在Controller中獲取當(dāng)前登錄的且被認(rèn)證通過的用戶。
以下 的 @CurrentUser
注解頭部添加了@AuthenticationPrincipal
注解攒暇。
package com.example.polls.security;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import java.lang.annotation.*;
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal
public @interface CurrentUser {
}
我們創(chuàng)建了一個(gè)元注解(注解的注解)的目的是為了在我們項(xiàng)目中不用到處與Spring Security的相關(guān)注解打交道土匀。減少了對(duì)Spring Security的依賴。如果我們決定不再使用Spring Security了形用,我們也可以輕松的更改@CurrentUser
來(lái)做到就轧。
編寫登錄和注冊(cè)API
伙計(jì)們,我們已經(jīng)把我們需要的安全配置都搞定了田度,是時(shí)候來(lái)編寫登錄和注冊(cè)的API了妒御。
但在定義這些API之前,我們需要先定義API需要使用到的 請(qǐng)求體和返回體镇饺。
所有的這些請(qǐng)求體和返回體我們定義在包com.example.polls.payload
中乎莉。
Request Payloads
1. LoginRquest
package com.example.polls.payload;
import javax.validation.constraints.NotBlank;
public class LoginRequest {
@NotBlank
private String usernameOrEmail;
@NotBlank
private String password;
public String getUsernameOrEmail() {
return usernameOrEmail;
}
public void setUsernameOrEmail(String usernameOrEmail) {
this.usernameOrEmail = usernameOrEmail;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
2. SignUpRequest
package com.example.polls.payload;
import javax.validation.constraints.*;
public class SignUpRequest {
@NotBlank
@Size(min = 4, max = 40)
private String name;
@NotBlank
@Size(min = 3, max = 15)
private String username;
@NotBlank
@Size(max = 40)
@Email
private String email;
@NotBlank
@Size(min = 6, max = 20)
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Response Payloads
1. JwtAuthenticationResponse
package com.example.polls.payload;
public class JwtAuthenticationResponse {
private String accessToken;
private String tokenType = "Bearer";
public JwtAuthenticationResponse(String accessToken) {
this.accessToken = accessToken;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
}
2. ApiResponse
package com.example.polls.payload;
public class ApiResponse {
private Boolean success;
private String message;
public ApiResponse(Boolean success, String message) {
this.success = success;
this.message = message;
}
public Boolean getSuccess() {
return success;
}
public void setSuccess(Boolean success) {
this.success = success;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
自定義業(yè)務(wù)異常
請(qǐng)求非法或一些超預(yù)期情況的發(fā)生時(shí),API會(huì)拋出異常。
我們需要在返回體中展現(xiàn)出不同類型的異常應(yīng)有對(duì)應(yīng)的 HTTP code梦鉴。
讓我們用@ResponseStatus注解開始定義異常吧(所有異常相關(guān)代碼都定義在com.example.polls.exception
)
1. AppException
package com.example.polls.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public class AppException extends RuntimeException {
public AppException(String message) {
super(message);
}
public AppException(String message, Throwable cause) {
super(message, cause);
}
}
2. BadRequestException
package com.example.polls.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException {
public BadRequestException(String message) {
super(message);
}
public BadRequestException(String message, Throwable cause) {
super(message, cause);
}
}
3. ResourceNotFoundException
package com.example.polls.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
private String resourceName;
private String fieldName;
private Object fieldValue;
public ResourceNotFoundException( String resourceName, String fieldName, Object fieldValue) {
super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue));
this.resourceName = resourceName;
this.fieldName = fieldName;
this.fieldValue = fieldValue;
}
public String getResourceName() {
return resourceName;
}
public String getFieldName() {
return fieldName;
}
public Object getFieldValue() {
return fieldValue;
}
}
Authentication Controller
最后李茫,AuthController
包含了登錄和注冊(cè)的接口。(所有Controller應(yīng)放在包com.example.polls.controller
下)-
package com.example.polls.controller;
import com.example.polls.exception.AppException;
import com.example.polls.model.Role;
import com.example.polls.model.RoleName;
import com.example.polls.model.User;
import com.example.polls.payload.ApiResponse;
import com.example.polls.payload.JwtAuthenticationResponse;
import com.example.polls.payload.LoginRequest;
import com.example.polls.payload.SignUpRequest;
import com.example.polls.repository.RoleRepository;
import com.example.polls.repository.UserRepository;
import com.example.polls.security.JwtTokenProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
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.security.crypto.password.PasswordEncoder;
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;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import javax.validation.Valid;
import java.net.URI;
import java.util.Collections;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
UserRepository userRepository;
@Autowired
RoleRepository roleRepository;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
JwtTokenProvider tokenProvider;
@PostMapping("/signin")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsernameOrEmail(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = tokenProvider.generateToken(authentication);
return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
}
@PostMapping("/signup")
public ResponseEntity<?> registerUser(@Valid @RequestBody SignUpRequest signUpRequest) {
if(userRepository.existsByUsername(signUpRequest.getUsername())) {
return new ResponseEntity(new ApiResponse(false, "Username is already taken!"),
HttpStatus.BAD_REQUEST);
}
if(userRepository.existsByEmail(signUpRequest.getEmail())) {
return new ResponseEntity(new ApiResponse(false, "Email Address already in use!"),
HttpStatus.BAD_REQUEST);
}
// Creating user's account
User user = new User(signUpRequest.getName(), signUpRequest.getUsername(),
signUpRequest.getEmail(), signUpRequest.getPassword());
user.setPassword(passwordEncoder.encode(user.getPassword()));
Role userRole = roleRepository.findByName(RoleName.ROLE_USER)
.orElseThrow(() -> new AppException("User Role not set."));
user.setRoles(Collections.singleton(userRole));
User result = userRepository.save(user);
URI location = ServletUriComponentsBuilder
.fromCurrentContextPath().path("/api/users/{username}")
.buildAndExpand(result.getUsername()).toUri();
return ResponseEntity.created(location).body(new ApiResponse(true, "User registered successfully"));
}
}
開啟跨域
react需要從自己的端訪問到服務(wù)端的這些API肥橙。為了允許跨域訪問這些接口魄宏,我們需要?jiǎng)?chuàng)建WebMvcConfig
類在包com.example.polls.config
下。
package com.example.polls.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final long MAX_AGE_SECS = 3600;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("HEAD", "OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE")
.maxAge(MAX_AGE_SECS);
}
}
檢測(cè)當(dāng)前配置并啟動(dòng)程序
如果你根據(jù)以上一步步完成了下來(lái)存筏,你的項(xiàng)目結(jié)構(gòu)應(yīng)該如下-
你可以在當(dāng)前項(xiàng)目根目錄下宠互,用終端輸入一下命令來(lái)啟動(dòng)項(xiàng)目:
mvn spring-boot:run
測(cè)試登錄和注冊(cè)API
注冊(cè)
登錄
調(diào)用受保護(hù)API
一旦你從登錄接口獲得了返回的token,你就可以通過把token放到請(qǐng)求頭的Authorization
中去調(diào)用受保護(hù)的API椭坚,就像下面這樣-
Authorization: Bearer <accessToken>
JwtAuthenticationFilter
將會(huì)從請(qǐng)求頭讀取token予跌,驗(yàn)證它而判斷是否有權(quán)限去訪問這些API。
下一步是什么善茎?
哦吼券册!我們?cè)诒疚恼掠懻摿撕芏唷2⑹褂肧pring Security和JWT構(gòu)建了可靠的身份認(rèn)證和授權(quán)邏輯垂涯。感謝一直閱讀到最后烁焙。
在下一章中,我們會(huì)編寫創(chuàng)建調(diào)查和投票耕赘,獲取用戶等API骄蝇。