Spring Security+JWT+RSA分布式版实战
注意下面所有代码类中代码,导包缺失序列化工具类、用户缓存工具类、JWT、RSA、统一异常、统一响应格式等工具类,请到博主其他文章中获取。
文章目录
1、导包
implementation "org.springframework.boot:spring-boot-starter-security:2.6.5"
implementation "io.jsonwebtoken:jjwt-api:0.10.7"
implementation "io.jsonwebtoken:jjwt-impl:0.10.7"
implementation "io.jsonwebtoken:jjwt-jackson:0.10.7"
implementation "joda-time:joda-time:2.10.1"
注意:博主使用的不是Maven,是Gradle。
2、配置类
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
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.builders.WebSecurity;
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.web.authentication.logout.LogoutSuccessHandler;
/**
* @author :
* @date :Created in 14:38 2022/9/7
* @description :
* @version: 1.0
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final RsaKeyProperties prop;
private final SecurityUserDetailsService securityUserDetailsService;
private final BCryptPasswordEncoder passwordEncoder;
private final LogoutSuccessHandler logoutSuccessHandler;
private final VerifyAuthenticationEntryPoint verifyAuthenticationEntryPoint;
private final IUserCacheService iUserCacheService;
private final LoginUserAccessDeniedHandler accessDeniedHandler;
private final LoginFailureHandler loginFailureHandler;
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(permitUrls);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
JwtLoginFilter jwtLoginFilter = new JwtLoginFilter(super.authenticationManager());
jwtLoginFilter.setAuthenticationFailureHandler(loginFailureHandler);
http.cors().and().csrf().disable();
http.authorizeRequests()
.anyRequest().authenticated()
.and().addFilter(jwtLoginFilter)
.addFilter(new JwtVerifyFilter(super.authenticationManager(), prop, iUserCacheService, verifyAuthenticationEntryPoint))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().exceptionHandling()
.authenticationEntryPoint(verifyAuthenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
.and().formLogin().permitAll()
.and().logout().permitAll()
.logoutSuccessHandler(logoutSuccessHandler)
;
}
/**
* 认证授权控制器 (管理者 提供者 认证者 授权者)
* @param auth
* @return void
* @Author
* @Date Created in 10:31 2022/9/6
* @Description
**/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(securityUserDetailsService).passwordEncoder(passwordEncoder);
}
/**
* 放过Url
* @Author
* @Date Created in 11:34 2022/9/11
* @Description
* @param
* @return
**/
String[] permitUrls = new String[]{
"/" + VERSION_NO + "/test/**"
};
}
3、UserDetailsService和SecurityUser
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.security.authentication.BadCredentialsException;
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.Component;
/**
*
* @Author
* @Date Created in 16:57 2022/9/31
* @Description
* @param
* @return
**/
@Component
@Slf4j
@RequiredArgsConstructor
public class SecurityUserDetailsService implements UserDetailsService {
private final IUserService userService;
private final IUserCacheService iUserCacheService;
private final RsaKeyProperties prop;
@Override
public UserDetails loadUserByUsername(String username) {
User user = userService.findByUsername(username);
checkUserInfo(user);
return new SecurityUser(user, getJti(user));
}
private void checkUserInfo(User user) {
if(ObjectUtils.isEmpty(user)){
log.info("登录失败,查询不到用户信息");
throw new UsernameNotFoundException("账户不存在");
}
if(ObjectUtils.isEmpty(user.getRoles())){
log.info("登录失败,未绑定角色,不允许登录!");
throw new BadCredentialsException("用户未绑定角色,不允许登录,请联系管理员!");
}
}
private String getJti(User user) {
JwtClaim jwtClaim = JwtClaim.toJwtClaim(user);
String jwt = JwtUtils.generateTokenExpireInMinutes(jwtClaim, prop.getPrivateKey(), 24 * 60 * 7);
String jti = JwtUtils.getInfoFromToken(jwt, prop.getPublicKey(), JwtClaim.class).getId();
iUserCacheService.cacheJwtAndUser(jti, jwt, user);
return jti;
}
}
/**
* @author :
* @date :Created in 9:46 2022/6/29
* @description :
* @version: 1.0
*/
public class SecurityUser implements UserDetails, Serializable {
private User user;
private String jti;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authList = new ArrayList<>();
for(Role role:this.user.getRoles()){
//1.1角色关键词
authList.add(new SimpleGrantedAuthority(role.getKeyword()));
for (Permission permission:role.getPermissions()){
//1.2权限关键词
authList.add(new SimpleGrantedAuthority(permission.getKeyword()));
}
}
return authList;
}
@Override
public String getPassword() {
return this.user.getPassword();
}
@Override
public String getUsername() {
return ObjectUtils.isEmpty(this.user) ? "" :this.user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return UserStatusEnum.EFFECTIVE.getCode().equals(this.user.getState());
}
public SecurityUser(User user, String jti) {
this.user = user;
this.jti = jti;
}
public SecurityUser() {
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String getJti() {
return jti;
}
public void setJti(String jti) {
this.jti = jti;
}
}
4、登录处理
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
/**
* @author :
* @date :Created in 16:25 2022/9/11
* @description :
* @version: 1.0
*/
@Slf4j
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JwtLoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@SneakyThrows
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response){
log.info("[login]接收到请求");
User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
log.info("[login]username:{}", user.getUsername());
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
return authenticationManager.authenticate(authRequest);
}
@Override
public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
log.info("[login]认证成功");
response.addHeader(HttpHeaders.AUTHORIZATION, JwtUtils.BEARER_PREFIX + ((SecurityUser)authResult.getPrincipal()).getJti());
response.setContentType(APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_OK);
log.info("[login]返回token");
ServletUtils.render(request, response, Result.success(null));
}
}
5、校验处理
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 检验(认证授权)
* @Author
* @Date Created in 15:33 2022/9/29
* @Description
* @param
* @return
**/
public class JwtVerifyFilter extends BasicAuthenticationFilter {
private RsaKeyProperties prop;
private IUserCacheService iUserCacheService;
private VerifyAuthenticationEntryPoint verifyAuthenticationEntryPoint;
public JwtVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop, IUserCacheService iUserCacheService, VerifyAuthenticationEntryPoint verifyAuthenticationEntryPoint) {
super(authenticationManager);
this.prop = prop;
this.iUserCacheService = iUserCacheService;
this.verifyAuthenticationEntryPoint = verifyAuthenticationEntryPoint;
}
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
String jtiInfo = request.getHeader(HttpHeaders.AUTHORIZATION);
checkJti(jtiInfo);
String jti = jtiInfo.replace(BEARER_PREFIX, "");
String jwt = iUserCacheService.getJwtByJti(jti);
checkJwt(jwt);
Payload<JwtClaim> payload = JwtUtils.getInfoFromToken(jwt, prop.getPublicKey(), JwtClaim.class);
UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken("", null, null);;
if (!payload.getUserInfo().getClientType().equals(ClientTypeEnum.TO_C.getId())) {
SecurityUser securityUser = new SecurityUser(iUserCacheService.getUserAndRenewalByUserNameAndJti(payload.getUserInfo().getUsername(), jti), jti);
authResult = new UsernamePasswordAuthenticationToken(securityUser, securityUser.getPassword(), securityUser.getAuthorities());
}
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}catch (AuthenticationException ex){
verifyAuthenticationEntryPoint.commence(request, response, ex);
}
}
private void checkJwt(String jwt) {
if (StringUtils.isBlank(jwt)) {
throw new InsufficientAuthenticationException(VerifyAuthenticationEntryPoint.VerifyConstants + ExceptionCodeEnum.EXPIRED_TOKEN.getDesc());
}
}
private void checkJti(String jtiInfo) {
if (StringUtils.isBlank(jtiInfo) || !jtiInfo.startsWith(BEARER_PREFIX)) {
throw new InsufficientAuthenticationException(VerifyAuthenticationEntryPoint.VerifyConstants + ERROR_TOKEN.getDesc());
}
}
}
6、登录失败处理
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author :
* @date :Created in 15:20 2022/9/10
* @description :
* @version: 1.0
*/
@Slf4j
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
private final String UserConstants = "用户";
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
String message;
if(e.getMessage().contains(UserConstants)){
ServletUtils.render(request, response, Result.of(ExceptionCodeEnum.PARAM_ERROR, e.getMessage()));
return;
}
if (e instanceof BadCredentialsException) {
message = "用户名不存在或密码错误!";
} else if (e instanceof AccountStatusException) {
message = "用户状态不可用!";
} else {
message = "登录失败!";
log.error("[登录失败]message:{}", ExceptionUtils.getStackTrace(e));
}
log.info("[登录失败] - {}", message);
ServletUtils.render(request, response, Result.of(ExceptionCodeEnum.PARAM_ERROR, message));
}
}
7、权限不足处理
mport lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author :
* @date :Created in 16:30 2022/9/8
* @description :
* @version: 1.0
*/
@Slf4j
@Component
public class LoginUserAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.warn("用户权限不足,访问[{}]失败", request.getRequestURI());
ServletUtils.render(request, response, Result.of(ExceptionCodeEnum.FORBIDDEN_OPERATION, "权限不足!"));
}
}
8、校验失败处理
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.security.authentication.InsufficientAuthenticationException;
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;
import java.io.IOException;
/**
* @author :
* @date :Created in 16:37 2022/9/8
* @description :
* @version: 1.0
*/
@Component
@Slf4j
public class VerifyAuthenticationEntryPoint implements AuthenticationEntryPoint {
public static final String VerifyConstants = "验证异常:";
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
log.warn("用户认证,访问[{}]失败,AuthenticationException={}", request.getRequestURI(), e);
String message;
if (e instanceof InsufficientAuthenticationException) {
message = "请重新登录!";
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
} else {
message = "验证失败!";
log.error("[验证失败]message:{}", ExceptionUtils.getStackTrace(e));
}
log.info("[验证失败] - {}", message);
ServletUtils.render(request, response, Result.of(ExceptionCodeEnum.PARAM_ERROR, message));
}
}
9、退出登录
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author :
* @date :Created in 16:28 2022/9/8
* @description :
* @version: 1.0
*/
@Component
@Slf4j
@AllArgsConstructor
public class LogoutSuccessHandler implements org.springframework.security.web.authentication.logout.LogoutSuccessHandler {
private IUserCacheService iUserCacheService;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("[logout]用户退出登录");
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header == null || !header.startsWith(BEARER_PREFIX)) {
log.info("[logout]用户退出登录成功");
} else {
String jti = header.replace(BEARER_PREFIX, "");
iUserCacheService.deleteJwtAndUser(jti, null);
}
log.info("[logout]用户退出登录成功");
ServletUtils.render(request, response, Result.success(null));
}
}
10、前后端分离工具类
import com.alibaba.fastjson.JSONObject;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author :
* @date :Created in 16:30 2022/9/11
* @description :
* @version: 1.0
*/
public class ServletUtils {
/**
* 渲染到客户端
*
* @param object 待渲染的实体类,会自动转为json
*/
public static void render(HttpServletRequest request, HttpServletResponse response, Object object) throws IOException {
// 允许跨域
response.setHeader("Access-Control-Allow-Origin", "*");
// 允许自定义请求头token(允许head跨域)
response.setHeader("Access-Control-Allow-Headers", "token, Accept, Origin, X-Requested-With, Content-Type, Last-Modified");
response.setHeader("Content-type", "application/json;charset=UTF-8");
response.getWriter().print(JSONObject.toJSONString(object));
}
/**
* 获取request
*/
public static HttpServletRequest getRequest() {
return getRequestAttributes().getRequest();
}
/**
* 获取response
*/
public static HttpServletResponse getResponse() {
return getRequestAttributes().getResponse();
}
public static ServletRequestAttributes getRequestAttributes() {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return (ServletRequestAttributes) attributes;
}
}