1. 新建Spring Boot 项目
1.1 创建 Controller
@RestController
@RequestMapping(value = "/api")
public class UserController {
@Autowired
private UserService userService;
@GetMapping(value = "/user/{username}")
public User getUserByUsername(@PathVariable("username") String username) {
return userService.findUserByUsername(username);
}
}
1.2 创建 Service 及 Entity
public interface UserService {
User findUserByUsername(String username);
}
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Override
public User findUserByUsername(String username) {
log.info("Executing find user by username [{}]", username);
return new User(username, "123456", "1");
}
}
@Data
public class User {
private String username;
private String password;
private String authority;
public User() {
}
public User(User user) {
this.username = user.getUsername();
this.password = user.getPassword();
this.authority = user.getAuthority();
}
}
1.3 pom 文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
2. 添加 Spring Security + JWT
Spring Security 验证身份的方式是利用 Filter,再加上 HttpServletRequest 的一些信息进行过滤,基本角色如下:
- 类 Authentication 保存的是身份认证信息。
- 类 AuthenticationProvider 提供身份认证途径。
- 类 AuthenticationManager 保存的 AuthenticationProvider 集合,并调用 AuthenticationProvider 进行身份认证。
2.1 pom 文件
<!-- spring security 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- jwt 依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!-- swagger-ui 依赖 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger-version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger-version}</version>
</dependency>
2.3 创建请求实体类
public class LoginRequest {
private String username;
private String password;
@JsonCreator
public LoginRequest(@JsonProperty("username") String username, @JsonProperty("password") String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}
2.3 创建认证过滤器
AbstractAuthenticationProcessingFilter 是一个抽象类,主要的功能是身份认证,子类实现 attemptAuthentication 方法提供身份认证的具体实现,根据 HttpServletRequest 等信息进行身份认证,并返回 Authentication 对象、 null、异常,分别表示认证成功返回的身份认证信息、需要其他 Filter 继续进行身份认证、认证失败。
public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {
private final AuthenticationSuccessHandler successHandler;
private final AuthenticationFailureHandler failureHandler;
private final ObjectMapper objectMapper;
public RestLoginProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler, AuthenticationFailureHandler failureHandler, ObjectMapper mapper) {
super(defaultProcessUrl);
this.successHandler = successHandler;
this.failureHandler = failureHandler;
this.objectMapper = mapper;
}
/**
* 具体身份认证方法
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
LoginRequest loginRequest;
try {
loginRequest = objectMapper.readValue(request.getReader(), LoginRequest.class);
} catch (Exception e) {
throw new AuthenticationServiceException("Invalid login request payload");
}
if (StringUtils.isEmpty(loginRequest.getUsername()) || StringUtils.isEmpty(loginRequest.getPassword())) {
throw new AuthenticationServiceException("Username or Password not provided");
}
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());
// 调用注册到AuthenticationManager中的AuthenticationProvider的认证方法authenticate
return this.getAuthenticationManager().authenticate(token);
}
/**
* 认证成功回调
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
successHandler.onAuthenticationSuccess(request, response, authResult);
}
/**
* 认证失败回调
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
failureHandler.onAuthenticationFailure(request, response, failed);
}
}
2.4 创建自定义AuthenticationProvider
@Component
public class RestAuthenticationProvider implements AuthenticationProvider {
private final UserService userService;
private final BCryptPasswordEncoder encoder; // 密码加密
@Autowired
public RestAuthenticationProvider(final UserService userService, final BCryptPasswordEncoder encoder) {
this.encoder = encoder;
this.userService = userService;
}
/**
* 身份认证方法
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.notNull(authentication, "No authentication data provided");
// 用户名
String username = authentication.getName();
// 密码
String password = (String) authentication.getCredentials();
// 查询用户信息
User user = userService.getUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found: " + username);
}
if (!encoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("Authentication Failed. Username or Password not valid.");
}
SecurityUser securityUser = new SecurityUser(user);
// 认证成功回调获取安全用户信息是在这里实例化的(UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities))
return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
}
/**
* 检查authentication的类型是不是这个AuthenticationProvider支持的
*/
@Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
}
2.5 创建成功和失败返回
/**
* 认证成功回调
*/
@Component
public class RestAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final ObjectMapper mapper;
private final JwtTokenFactory tokenFactory;
@Autowired
public RestAuthenticationSuccessHandler(final ObjectMapper mapper, final JwtTokenFactory tokenFactory) {
this.mapper = mapper;
this.tokenFactory = tokenFactory;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Map<String, Object> hashMap = new ConcurrentHashMap<>();
// 获取安全的用户信息
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
// 生成token(具体方法见后面)
String token = tokenFactory.createAccessJwtToken(securityUser);
hashMap.put("token", token);
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
mapper.writeValue(response.getWriter(),hashMap);
}
}
/**
* 认证失败回调
*/
@Component
public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Autowired
private ObjectMapper mapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
// 暂时未处理失败回调
Map<String, Object> hashMap = new ConcurrentHashMap<>();
hashMap.put("code", 401);
hashMap.put("status", false);
hashMap.put("message", "User not logged in");
hashMap.put("timestamp", new Date());
mapper.writeValue(response.getWriter(), hashMap);
}
}
3. 创建 JWT 认证
3.1 创建token包装类
3.2 创建JWT认证过滤器
/**
* jwt认证的过滤器
*/
public class JwtTokenAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
private final AuthenticationFailureHandler failureHandler;
public JwtTokenAuthenticationProcessingFilter(AuthenticationFailureHandler failureHandler, RequestMatcher matcher) {
super(matcher);
this.failureHandler = failureHandler;
}
/**
* 身份认证
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
RawAccessJwtToken token = new RawAccessJwtToken(extract(request));
return getAuthenticationManager().authenticate(new JwtAuthenticationToken(token));
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
chain.doFilter(request, response);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
failureHandler.onAuthenticationFailure(request, response, failed);
}
/**
* 解析http请求中token
*/
private String extract(HttpServletRequest request) {
String header = request.getHeader(SotaSecurityConfiguration.JWT_TOKEN_HEADER_PARAM);
if (StringUtils.isEmpty(header)) {
throw new AuthenticationServiceException("Authorization header cannot be blank!");
}
if (header.length() < HEADER_PREFIX.length()) {
throw new AuthenticationServiceException("Invalid authorization header size.");
}
return header.substring(HEADER_PREFIX.length(), header.length());
}
}
3.3 创建 JWT 身份认证类
/**
* jwt认证的具体实现
*/
@Component
@SuppressWarnings("unchecked")
public class JwtAuthenticationProvider implements AuthenticationProvider {
private final JwtTokenFactory tokenFactory;
@Autowired
public JwtAuthenticationProvider(JwtTokenFactory tokenFactory) {
this.tokenFactory = tokenFactory;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials();
SecurityUser securityUser = tokenFactory.parseAccessJwtToken(rawAccessToken);
return new JwtAuthenticationToken(securityUser);
}
@Override
public boolean supports(Class<?> authentication) {
return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
}
}
4. Spring Security 配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SotaSecurityConfiguration extends WebSecurityConfigurerAdapter {
public static final String JWT_TOKEN_QUERY_PARAM = "token";
public static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization";
public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/rest/**";
public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login";
public static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[]{"/index.html", "/static/**", "/api/noauth/**", "/webjars/**"};
@Autowired
private ObjectMapper objectMapper;
@Autowired
private AuthenticationSuccessHandler successHandler;
@Autowired
private AuthenticationFailureHandler failureHandler;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
@Qualifier("jwtHeaderTokenExtractor")
private TokenExtractor jwtHeaderTokenExtractor;
@Autowired
private RestAuthenticationProvider restAuthenticationProvider;
@Autowired
private JwtAuthenticationProvider jwtAuthenticationProvider;
@Bean
protected BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 注册Authentication Provider,使用自定义身份验证组件
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(restAuthenticationProvider);
auth.authenticationProvider(jwtAuthenticationProvider);
}
/**
* 设置 HTTP 验证规则
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers()
// 禁用缓存
.cacheControl().disable().frameOptions().disable()
.and()
.cors()
.and()
// 禁用csrf
.csrf().disable()
.exceptionHandling()
.and()
// 使用token所以禁用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 不需要认证接口
.antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll()
// 需要认证接口
.antMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated()
.and()
// 登录过滤器
.addFilterBefore(buildRestLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
// JWT认证过滤器
.addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
}
/**
* 身份验证过滤器注册
*/
@Bean
protected RestLoginProcessingFilter buildRestLoginProcessingFilter() throws Exception {
RestLoginProcessingFilter filter = new RestLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper);
filter.setAuthenticationManager(this.authenticationManager);
return filter;
}
/**
* jwt认证注册
*/
@Bean
protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
List<String> pathsToSkip = new ArrayList(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS));
pathsToSkip.addAll(Arrays.asList(FORM_BASED_LOGIN_ENTRY_POINT));
SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
JwtTokenAuthenticationProcessingFilter filter = new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher);
filter.setAuthenticationManager(this.authenticationManager);
return filter;
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
5. JWT token 工具类
实际生产环境中,不建议使用敏感信息加密生成token
5.1 JWT 配置类
@Configuration
@ConfigurationProperties(prefix = "security.jwt")
public class SotaJwtSettings {
// 将在此之后过期
private Integer tokenExpirationTime;
// 令牌发行人
private String tokenIssuer;
// 密钥用于签名
private String tokenSigningKey;
// 可以在这段时间内刷新
private Integer refreshTokenExpTime;
public Integer getRefreshTokenExpTime() {
return refreshTokenExpTime;
}
public void setRefreshTokenExpTime(Integer refreshTokenExpTime) {
this.refreshTokenExpTime = refreshTokenExpTime;
}
public Integer getTokenExpirationTime() {
return tokenExpirationTime;
}
public void setTokenExpirationTime(Integer tokenExpirationTime) {
this.tokenExpirationTime = tokenExpirationTime;
}
public String getTokenIssuer() {
return tokenIssuer;
}
public void setTokenIssuer(String tokenIssuer) {
this.tokenIssuer = tokenIssuer;
}
public String getTokenSigningKey() {
return tokenSigningKey;
}
public void setTokenSigningKey(String tokenSigningKey) {
this.tokenSigningKey = tokenSigningKey;
}
5.2 token 工具类
@Component
public class JwtTokenFactory {
private static final String SCOPES = "scopes";
private static final String USER_ID = "id";
private static final String USER_NAME = "username";
private static final String PASSWORD = "password";
private final SotaJwtSettings settings;
@Autowired
public JwtTokenFactory(SotaJwtSettings settings) {
this.settings = settings;
}
/**
* 创建token
*/
public String createAccessJwtToken(SecurityUser securityUser) {
if (StringUtils.isEmpty(securityUser.getUsername())) {
throw new IllegalArgumentException("Cannot create JWT Token without username/email");
}
if (securityUser.getAuthority() == null) {
throw new IllegalArgumentException("User doesn't have any privileges");
}
Claims claims = Jwts.claims().setSubject(securityUser.getEmail());
claims.put(SCOPES, securityUser.getAuthorities().stream().map(s -> s.getAuthority()).collect(Collectors.toList()));
claims.put(USER_ID, securityUser.getId());
claims.put(USER_NAME, securityUser.getUsername());
claims.put(PASSWORD, securityUser.getPassword());
Instant instant = Instant.now();
// 生成token
String token = Jwts.builder()
.setClaims(claims)
.setIssuer(settings.getTokenIssuer())
.setIssuedAt(Date.from(instant))
.setExpiration(Date.from(instant.plusSeconds(settings.getTokenExpirationTime())))
.signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey())
.compact();
return token;
}
/**
* 解析token获取用户信息
*/
public SecurityUser parseAccessJwtToken(RawAccessJwtToken rawAccessToken) {
Jws<Claims> jwsClaims = rawAccessToken.parseClaims(settings.getTokenSigningKey());
Claims claims = jwsClaims.getBody();
String subject = claims.getSubject();
List<String> scopes = claims.get(SCOPES, List.class);
if (scopes == null || scopes.isEmpty()) {
throw new IllegalArgumentException("JWT Token doesn't have any scopes");
}
SecurityUser securityUser = new SecurityUser();
securityUser.setId(claims.get(USER_ID, Integer.class));
securityUser.setUsername(claims.get(USER_NAME, String.class));
securityUser.setPassword(claims.get(PASSWORD, String.class));
securityUser.setEmail(subject);
securityUser.setAuthority(scopes.get(0));
return securityUser;
}
}