違背的青春
今天寫下Spring Security
整合jwt
的一個簡單小Demo
,目的是登入後實現返回token
,其實整個過程很簡單。
匯入依賴
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
複製程式碼
首先建立一個JwtUser實現UserDetails
org.springframework.security.core.userdetails.UserDetails
先看一下這個介面的原始碼,其實很簡單
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
複製程式碼
這個是Spring Security
給我們提供的一個簡單的介面,因為我們需要通過SecurityContextHolder
去取得使用者憑證等等資訊,因為這個比較簡單,所以我們實際業務要來加上我們所需要的資訊。
public class JwtUser implements UserDetails {
private String username;
private String password;
private Integer state;
private Collection<? extends GrantedAuthority> authorities;
public JwtUser() {
}
public JwtUser(String username, String password, Integer state, Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.state = state;
this.authorities = authorities;
}
@Override
public String getUsername() {
return username;
}
@JsonIgnore
@Override
public String getPassword() {
return password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
//賬戶是否未過期
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
//賬戶是否未被鎖
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//是否啟用
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
}
複製程式碼
我這個其實也很簡單,只是一些使用者名稱,密碼狀態和許可權的集合這些。
編寫一個工具類來生成令牌等…
@Data
@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtTokenUtil implements Serializable {
private String secret;
private Long expiration;
private String header;
/**
* 從資料宣告生成令牌
*
* @param claims 資料宣告
* @return 令牌
*/
private String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + expiration);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret).compact();
}
/**
* 從令牌中獲取資料宣告
*
* @param token 令牌
* @return 資料宣告
*/
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 生成令牌
*
* @param userDetails 使用者
* @return 令牌
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>(2);
claims.put("sub", userDetails.getUsername());
claims.put("created", new Date());
return generateToken(claims);
}
/**
* 從令牌中獲取使用者名稱
*
* @param token 令牌
* @return 使用者名稱
*/
public String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 判斷令牌是否過期
*
* @param token 令牌
* @return 是否過期
*/
public Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return false;
}
}
/**
* 重新整理令牌
*
* @param token 原令牌
* @return 新令牌
*/
public String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put("created", new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 驗證令牌
*
* @param token 令牌
* @param userDetails 使用者
* @return 是否有效
*/
public Boolean validateToken(String token, UserDetails userDetails) {
JwtUser user = (JwtUser) userDetails;
String username = getUsernameFromToken(token);
return (username.equals(user.getUsername()) && !isTokenExpired(token));
}
}
複製程式碼
這個類就是一些生成令牌,驗證等等一些操作。具體看註釋~
編寫一個Filter
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader(jwtTokenUtil.getHeader());
if (authHeader != null && StringUtils.isNotEmpty(authHeader)) {
String username = jwtTokenUtil.getUsernameFromToken(authHeader);
log.info(username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authHeader, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
複製程式碼
這個其實就是用來驗證令牌的是否合法,由於今天這個Demo
是一個簡單的登入返回token
的過程,所以這個預設不會去執行裡面的邏輯。但是以後登陸後的操作就會執行裡面的邏輯了。
JwtUserDetailsServiceImpl
JwtUserDetailsServiceImpl
這個實現類是實現了UserDetailsService
,UserDetailsService
是Spring Security
進行身份驗證的時候會使用,我們這裡就一個載入使用者資訊的簡單方法,就是得到當前登入使用者的一些使用者名稱、密碼、使用者所擁有的角色等等一些資訊
@Slf4j
@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userMapper.selectByUserName(s);
if(user == null){
throw new UsernameNotFoundException(String.format("'%s'.這個使用者不存在", s));
}
List<SimpleGrantedAuthority> collect = user.getRoles().stream().map(Role::getRolename).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return new JwtUser(user.getUsername(), user.getPassword(), user.getState(), collect);
}
}
複製程式碼
編寫登入的業務實現類
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
public User findByUsername(String username) {
User user = userMapper.selectByUserName(username);
log.info("userserviceimpl"+user);
return user;
}
public RetResult login(String username, String password) throws AuthenticationException {
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
final Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
return new RetResult(RetCode.SUCCESS.getCode(),jwtTokenUtil.generateToken(userDetails));
}
}
複製程式碼
從上面可以看到login
方法,會根據使用者資訊然後返回一個token
給我們。
WebSecurityConfig
這個就是Spring Security
的配置類了
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurity extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder)throws Exception{
authenticationManagerBuilder.userDetailsService(this.userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean(name = 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.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
.and().headers().cacheControl();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
//讓Spring security放行所有preflight request
registry.requestMatchers(CorsUtils::isPreFlightRequest).permitAll();
}
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
final CorsConfiguration cors = new CorsConfiguration();
cors.setAllowCredentials(true);
cors.addAllowedOrigin("*");
cors.addAllowedHeader("*");
cors.addAllowedMethod("*");
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", cors);
return new CorsFilter(urlBasedCorsConfigurationSource);
}
}
複製程式碼
我們可以在這裡設定自定義的攔截規則,注意在Spring Security5.x
中我們要顯式注入AuthenticationManager
不然會報錯~
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
複製程式碼
目前基本的都已經完成了剩下的就是一些entity,controller
的程式碼了具體可以看我GitHub上的程式碼。
如果有任何見解或者我有寫錯的地方可以聯絡我…我一定改…