Spring Boot中使用使用Spring Security和JWT
目標
1.Token鑑權
2.Restful API
3.Spring Security+JWT
開始
自行新建Spring Boot工程
引入相關依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>1.5.9.RELEASE</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
User類
非常簡單的使用者模型,將許可權整合到了使用者類中。
pacage com.domain
/** * 使用者模型 * * @author hackyo * Created on 2017/12/3 11:53. */ public class User {</span><span style="color: #0000ff">private</span><span style="color: #000000"> String id; </span><span style="color: #0000ff">private</span><span style="color: #000000"> String username; </span><span style="color: #0000ff">private</span><span style="color: #000000"> String password; </span><span style="color: #0000ff">private</span> List<String><span style="color: #000000"> roles; ...... 省略get、set方法 ......
}
IUserRepository類
需實現對使用者表的增刪改查,此處可採用任意資料庫,具體實現自行編寫。
package com.dao
/** * 使用者表操作介面 * * @author hackyo * Created on 2017/12/3 11:53. */ @Component public interface IUserRepository{</span><span style="color: #008000">/**</span><span style="color: #008000"> * 通過使用者名稱查詢使用者 * * </span><span style="color: #808080">@param</span><span style="color: #008000"> username 使用者名稱 * </span><span style="color: #808080">@return</span><span style="color: #008000"> 使用者資訊 </span><span style="color: #008000">*/</span><span style="color: #000000"> User findByUsername(String username);
}
JwtUser類
安全模組的使用者模型
package com.security;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
-
安全使用者模型
-
@author hackyo
-
Created on 2017/12/8 9:20.
*/
public class JwtUser implements UserDetails {private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;JwtUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.password = password;
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;
}@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
}
JwtTokenUtil類
Token工具類
這裡設定了金鑰為aaaaaaaa,有效期為2592000秒
package com.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
-
JWT工具類
-
@author hackyo
-
Created on 2017/12/8 9:20.
*/
@Component
public class JwtTokenUtil implements Serializable {/**
- 金鑰
*/
private final String secret = “aaaaaaaa”;
/**
- 從資料宣告生成令牌
- @param claims 資料宣告
- @return 令牌
*/
private String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + 2592000L * 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類
使用者驗證方法類
package com.security;
import com.safepass.dao.IUserRepository;
import com.safepass.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 java.util.stream.Collectors;
/**
-
使用者驗證方法
-
@author hackyo
-
Created on 2017/12/8 9:18.
*/
@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {private IUserRepository userRepository;
@Autowired
public JwtUserDetailsServiceImpl(IUserRepository userRepository) {
this.userRepository = userRepository;
}@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(String.format(“No user found with username ‘%s’.”, username));
} else {
return new JwtUser(user.getUsername(), user.getPassword(), user.getRoles().stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
}
}
}
JwtAuthenticationTokenFilter類
Token過濾器實現
package com.security;
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.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
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;
/**
-
Token過濾器
-
@author hackyo
-
Created on 2017/12/8 9:28.
*/
@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;
}@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader(“Authorization”);
String tokenHead = "Bearer ";
if (authHeader != null && authHeader.startsWith(tokenHead)) {
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(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
EntryPointUnauthorizedHandler類
自定義了身份驗證失敗的返回值
package com.security;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
-
自定401返回值
-
@author hackyo
-
Created on 2017/12/9 20:10.
*/
@Component
public class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint {@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
response.setHeader(“Access-Control-Allow-Origin”, “*”);
response.setStatus(401);
}
}
RestAccessDeniedHandler類
自定了許可權不足的返回值
package com.security;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
-
自定403返回值
-
@author hackyo
-
Created on 2017/12/9 20:10.
*/
@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) {
response.setHeader(“Access-Control-Allow-Origin”, “*”);
response.setStatus(403);
}
}
WebSecurityConfig類
安全配置類
這裡設定了禁止訪問所有地址,除了用於驗證身份的/user/**地址
同時密碼的加密方式為BCrypt
package com.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
-
安全模組配置
-
@author hackyo
-
Created on 2017/12/8 9:15.
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {private UserDetailsService userDetailsService;
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;
private RestAccessDeniedHandler restAccessDeniedHandler;
private PasswordEncoder passwordEncoder;@Autowired
public WebSecurityConfig(UserDetailsService userDetailsService, JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter, EntryPointUnauthorizedHandler entryPointUnauthorizedHandler, RestAccessDeniedHandler restAccessDeniedHandler) {
this.userDetailsService = userDetailsService;
this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
this.entryPointUnauthorizedHandler = entryPointUnauthorizedHandler;
this.restAccessDeniedHandler = restAccessDeniedHandler;
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()
.antMatchers(HttpMethod.OPTIONS, “/").permitAll()
.antMatchers("/user/”).permitAll()
.anyRequest().authenticated()
.and().headers().cacheControl();
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
httpSecurity.exceptionHandling().authenticationEntryPoint(entryPointUnauthorizedHandler).accessDeniedHandler(restAccessDeniedHandler);}
}
IUserService類
定義使用者的基本操作
package com.service;
import com.domain.User;
/**
-
使用者操作介面
-
@author hackyo
-
Created on 2017/12/3 11:53.
*/
public interface IUserService {/**
- 使用者登入
- @param username 使用者名稱
- @param password 密碼
- @return 操作結果
*/
String login(String username, String password);
/**
- 使用者註冊
- @param user 使用者資訊
- @return 操作結果
*/
String register(User user);
/**
- 重新整理金鑰
- @param oldToken 原金鑰
- @return 新金鑰
*/
String refreshToken(String oldToken);
}
UserServiceImpl類
IUserService的實現類,註冊時會將使用者許可權設定為ROLE_USER,同時將密碼使用BCrypt加密
package com.service.impl;
import com.dao.IUserRepository;
import com.domain.User;
import com.security.JwtTokenUtil;
import com.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
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.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
-
使用者操作介面實現
-
@author hackyo
-
Created on 2017/12/3 11:53.
*/
@Service
public class UserServiceImpl implements IUserService {private AuthenticationManager authenticationManager;
private UserDetailsService userDetailsService;
private JwtTokenUtil jwtTokenUtil;
private IUserRepository userRepository;@Autowired
public UserServiceImpl(AuthenticationManager authenticationManager, UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, IUserRepository userRepository) {
this.authenticationManager = authenticationManager;
this.userDetailsService = userDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
this.userRepository = userRepository;
}@Override
public String login(String username, String password) {
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetails userDetails = userDetailsService.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));
List<String> roles = new ArrayList<>();
roles.add(“ROLE_USER”);
user.setRoles(roles);
userRepository.insert(user);
return “success”;
}@Override
public String refreshToken(String oldToken) {
String token = oldToken.substring("Bearer ".length());
if (!jwtTokenUtil.isTokenExpired(token)) {
return jwtTokenUtil.refreshToken(token);
}
return “error”;
}
}
UserController類
控制器,控制訪問
package com.controller;
import com.domain.User;
import com.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.*;
/**
-
使用者管理Controller
-
@author hackyo
-
Created on 2017/12/3 11:53.
*/
@CrossOrigin
@RestController
@RequestMapping(value = “/user”, produces = “text/html;charset=UTF-8”)
public class UserController {private IUserService userService;
@Autowired
public UserController(IUserService userService) {
this.userService = userService;
}/**
- 使用者登入
- @param username 使用者名稱
- @param password 密碼
- @return 操作結果
- @throws AuthenticationException 錯誤資訊
*/
@PostMapping(value = “/login”, params = {“username”, “password”})
public String getToken(String username, String password) throws AuthenticationException {
return userService.login(username, password);
}
/**
- 使用者註冊
- @param user 使用者資訊
- @return 操作結果
- @throws AuthenticationException 錯誤資訊
*/
@PostMapping(value = “/register”)
public String register(User user) throws AuthenticationException {
return userService.register(user);
}
/**
- 重新整理金鑰
- @param authorization 原金鑰
- @return 新金鑰
- @throws AuthenticationException 錯誤資訊
*/
@GetMapping(value = “/refreshToken”)
public String refreshToken(@RequestHeader String authorization) throws AuthenticationException {
return userService.refreshToken(authorization);
}
}
使用
只需要在方法或類上加註解即可實現賬號控制
例如,我們想控制該方法只允許使用者本人使用,#號表示方法的引數,可以在引數中加上@P('name')來指定名稱,同時也可直接使用模型,如user.username等
總之,其中可以寫入任何Spring EL
@PreAuthorize("#username == authentication.name") @GetMapping(value = "/getInfo") public String getInfo(String username) { return JSON.toJSONString(userService.getInfo(username)); }
另外也可以自定義控制註解,使用@PostFilter註解,並實現hasPermission類即可,同時需要在WebSecurityConfigurerAdapter中開啟。
測試
執行程式後,我們使用Postman進行測試
1.註冊
URL:http://localhost:8080/user/register
引數:username、password
返回success即為成功
2.登入
URL:http://localhost:8080/user/login
引數:username、password
可以看到伺服器將我們的Token返回了
3.重新整理Token
URL(GET方法):http://localhost:8080/user/refreshToken
引數:在Header中加入登入時返回的Token,注意,需要在Token前加上“Bearer ”,最後有個空格
Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE1MTMzMTE1NjMsInN1YiI6IjEyMyIsImNyZWF0ZWQiOjE1MTI3MDY3NjM3NjB9.baiY8QcbJgq4FQMC2piN1smbW57WjDDTiRVIL9hJeC_DcPgcyJweWqkS6g7825mPKFlByuUx7XN8nUOIszDVcw
可以看到伺服器給我們返回了新的Token,如果我們不加上Token的話,將無法訪問
參考: