【Spring Boot】IDEA + Maven + Spring Boot + JPA + Spring Security + JWT
在上篇部落格中,我們搭建好了一個使用者服務框架,本篇部落格緊接著使用者的業務場景的使用,在此基礎上整合spring security 和 jwt 實現使用者的登入,註冊以及許可權控制。
進行框架整合之前,我們先簡單瞭解一下Spring Security和JWT。
Spring Security :
Sping Security 是能夠為J2EE專案提供綜合性的安全訪問控制解決方案的安全框架。它依賴於Servlet過濾器。這些過濾器攔截進入請求,並且在應用程式處理該請求之前進行某些安全處理。
JWT(Json Web Token):
JSON Web Token(JWT)是一個非常輕巧的規範。這個規範允許我們使用JWT在使用者和伺服器之間傳遞安全可靠的資訊。JWT作為一個無狀態的授權校驗技術,非常適合於分散式系統架構,因為服務端不需要儲存使用者狀態,因此就無需採用redis等技術,在各個服務節點之間共享session資料。
下面記錄下整個整合過程:
一. 引入相關依賴:
<!-- Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>1.5.9.RELEASE</version>
</dependency >
<!-- Json Web Token -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
二. Repository層定義根據使用者名稱查詢使用者介面
/**
* 根據使用者名稱查詢使用者
* @param username
* @return
*/
User findByUsername(String username);
三. 新增安全使用者實體JwtUser 實現 Spring Security 的 UserDetails 介面
public class JwtUser implements UserDetails {
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
public JwtUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@JsonIgnore
@Override
public String getPassword() {
return password;
}
@JsonIgnore
@Override
public String getUsername() {
return username;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
}
四. 新增JwtToken 工具類,包含生成Token,驗證Token等方法。
@Component
public class JwtTokenUtil implements Serializable{
/**
* 金鑰
*/
private final String secret = "uqiauto";
/**
* 從資料宣告生成令牌
*
* @param claims 資料宣告
* @return 令牌
*/
private String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + 604800L * 1000);
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));
}
}
五. 新增使用者驗證方法類JwtUserDetailsServiceImpl,判斷使用者是否存在。
@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user =userRepository.findByUsername(username);
if(user==null){
throw new UsernameNotFoundException("使用者不存在");
}else{
// 使用者存在,給使用者授權
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
return new JwtUser(user.getUsername(), user.getPassword(), authorities);
}
}
}
六. Token過濾器實現,根據請求頭所帶有的token,驗證使用者的token是否正確,是否過期等。
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private UserDetailsService userDetailsService;
private JwtTokenUtil jwtTokenUtil;
@Autowired
public JwtAuthenticationTokenFilter(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil) {
this.userDetailsService = userDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
}
public JwtAuthenticationTokenFilter() {
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String authHeader = httpServletRequest.getHeader("Authorization");
String tokenHead = "Bearer ";
if (authHeader != null && authHeader.startsWith(tokenHead)) {
final String authToken = authHeader.substring(tokenHead.length());
String username = jwtTokenUtil.getUsernameFromToken(authToken);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
七. 新增安全配置類,除了使用者身份驗證地址外,其他請求均需要進行token驗證,密碼加密方式為BCrypt.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private UserDetailsService userDetailsService;
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
private PasswordEncoder passwordEncoder;
@Autowired
public WebSecurityConfig(UserDetailsService userDetailsService, JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter) {
this.userDetailsService = userDetailsService;
this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
this.passwordEncoder = new BCryptPasswordEncoder();
}
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(this.userDetailsService).passwordEncoder(passwordEncoder);
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
// 所有/uplus/user/ 的所有請求 都放行
.antMatchers("/uplus/user/**").permitAll()
// 所有請求都需要認證
.anyRequest().authenticated();
httpSecurity.headers().cacheControl();
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
八. IUserService中定義使用者操作介面,UserServiceImpl中實現
public interface IUserService{
/**
* 使用者登入
*
* @param username 使用者名稱
* @param password 密碼
* @return 操作結果
*/
String login(String username, String password) throws Exception;
/**
* 使用者註冊
*
* @param user 使用者資訊
* @return 操作結果
*/
String register(User user);
}
@Service
@Transactional
public class UserServiceImpl implements IUserService {
@Autowired
private AuthenticationManager authenticationManager;
private JwtUserDetailsServiceImpl jwtUserDetailsService;
private JwtTokenUtil jwtTokenUtil;
private UserRepository userRepository;
@Autowired
public UserServiceImpl( JwtUserDetailsServiceImpl jwtUserDetailsService, JwtTokenUtil jwtTokenUtil, UserRepository userRepository) {
this.jwtUserDetailsService = jwtUserDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
this.userRepository = userRepository;
}
@Override
public String login(String username, String password) throws Exception {
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(username);
return jwtTokenUtil.generateToken(userDetails);
}
@Override
public String register(User user) {
String username = user.getUsername();
if (userRepository.findByUsername(username) != null) {
return "使用者已存在";
}
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String rawPassword = user.getPassword();
user.setPassword(encoder.encode(rawPassword));
user.setDeleted(0);
user.setState(0);
userRepository.save(user);
return "success";
}
}
九. 使用者控制層,新增使用者註冊和登入請求,這裡的請求都不需要token認證。
@RestController
@RequestMapping("/uplus")
public class UserController {
@Autowired
private UserServiceImpl userService;
/**
* 使用者登入
*
* @param username 使用者名稱
* @param password 密碼
* @return 操作結果
* @throws AuthenticationException 錯誤資訊
*/
@PostMapping(value = "/user/login", params = {"username", "password"})
public String getToken(String username, String password) throws Exception {
return userService.login(username, password);
}
/**
* 使用者註冊
*
* @param user 使用者資訊
* @return 操作結果
* @throws AuthenticationException 錯誤資訊
*/
@PostMapping(value = "/user/register")
public String register(User user) throws AuthenticationException {
return userService.register(user);
}
十. 新增TestCotroller類,與UserController中請求分開,需要進行驗證才能請求。
@RestController
@RequestMapping("/uplus")
public class TestController {
@Autowired
private UserServiceImpl userService;
@Autowired
private UserRepository userRepository;
@PostMapping(value = "/test/getInfoById", params = {"id"} )
public User findById(Long id){
User userInfo=this.userService.findById(id);
return userInfo;
}
@PostMapping(value = "/test/findByUsername", params = {"username"} )
public User findByUsername(String username){
User userInfo=this.userRepository.findByUsername(username);
return userInfo;
}
}
十一. postman測試
1) 使用者註冊介面 (/uplus/user/register), 該介面在放行範圍內,所以不需要token認證。
2)使用者登入介面(/uplus/user/login),該介面在放行範圍內,同樣不需要token認證,最後結果會返回給我們一個token,在之後的其他請求,都需要將其設定在請求頭中。
3) 根據使用者id獲取使用者介面(uplus/test/findByUsername),該介面不在放行範圍內,所以需要token驗證,請求時新增請求頭引數,請求正確結果如下:
若請求中請求頭不帶token,則會返回沒有授權的結果,如下: