概要
springboot3.2.8,使用security登录认证
整体架构流程
代码思路
1.配置WebSecurityConfig类,配置正给http请求的认证和鉴权
2.自定义拦截器customizeAuthenticationFilter(),+账号密码验证UsernamePasswordAuthenticationFilter对用户认证
代码展示
- WebSecurityConfig 类,这里隐藏了一些内部使用的包。大家用可替换的包和方法函数解决。
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* 该类定义了SpringSecurity的主要流程配置
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
//从配置文件中的前缀获取配置
@ConfigurationProperties(prefix = "system")
@Data
public class WebSecurityConfig {
//handler定义了很多 通知
@Autowired
private CustomizeAuthenticationSuccessHandler customizeAuthenticationSuccessHandler;
@Autowired
private CustomizeAuthenticationFailureHandler customizeAuthenticationFailureHandler;
@Autowired
private CustomizeLogoutSuccessHandler customizeLogoutSuccessHandler;
@Autowired
//鉴权token过滤器
private JwtAuthenticationToKenFilter jwtAuthenticationToKenFilter;
@Autowired
private CustomizeAccessDeniedHandler customizeAccessDeniedHandler;
@Autowired
private CustomizeAuthenticationEntryPoint customizeAuthenticationEntryPoint;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private TokenService tokenService;
//定义了白名单
private String[] whiteList;
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{
httpSecurity
//禁用csrf 因为不使用session
.csrf(csrf->csrf.disable())
//禁用自带的跨域处理
.cors(cors->cors.disable())
//基于token 关掉session
.sessionManagement(session->session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
//权限路径控制
.authorizeHttpRequests(request->{
request
.requestMatchers(whiteList)
.permitAll()
/*.requestMatchers(HttpMethod.GET,"/","/*.html","/**.html","/**.css","/**.js","/profile/**","/grpc/**")
.permitAll()
.requestMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/webjars/**", "/v3/**")
.permitAll()
.requestMatchers("/auth/**")
.permitAll()*/
.anyRequest().authenticated();
})
//登录,这里将登录注释。依然可以通过POST带参数访问/login
/*.formLogin(login->{
login
//登录接口地址
.loginProcessingUrl("/login")
//允许所有用户登录
.permitAll()
//登录成功处理
.successHandler(customizeAuthenticationSuccessHandler)
//登录失败处理
.failureHandler(customizeAuthenticationFailureHandler);
})*/
//注销
.logout(logout->{
logout
.logoutUrl("/logout")
//注销成功处理
.logoutSuccessHandler(customizeLogoutSuccessHandler);
})
//配置自定义filter
.addFilterAt(customizeAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
//jwt权限过滤
// .addFilterBefore(jwtAuthenticationToKenFilter, CustomizeAuthenticationFilter.class)
//异常处理
.exceptionHandling(exception->{
exception
//认证失败处理
.authenticationEntryPoint(customizeAuthenticationEntryPoint)
//权限失败处理
.accessDeniedHandler(customizeAccessDeniedHandler);
});
return httpSecurity.build();
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 自定义filter配置
* 对用户信息验证,并设置成功和失败Handler
* @return
*/
@Bean
public CustomizeAuthenticationFilter customizeAuthenticationFilter(){
CustomizeAuthenticationFilter filter = new CustomizeAuthenticationFilter();
//必须设置authenticationManager
filter.setAuthenticationManager(authenticationManager());
filter.setAuthenticationSuccessHandler(customizeAuthenticationSuccessHandler);
filter.setAuthenticationFailureHandler(customizeAuthenticationFailureHandler);
return filter;
}
/**
* 注入userDetailsService和加解密Encoder,userDetailsService回去调用我们的实现类
* @return
*/
@Bean
public AuthenticationManager authenticationManager(){
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(bCryptPasswordEncoder());
ProviderManager pm = new ProviderManager(provider);
return pm;
}
}
- customizeAuthenticationFilter自定义拦截器,验证用户信息
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.io.IOException;
import java.util.Map;
/**
* 自定义filter
* 处理json传参
* 处理password解码
*/
public class CustomizeAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
if(request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
Map<String,String> map;
UsernamePasswordAuthenticationToken authRequest = null;
try{
//转换输入的json为map格式
map = new ObjectMapper().readValue(request.getInputStream(),Map.class);
String username = map.get(getUsernameParameter());
username = username != null ? username.trim() : "";
String password = map.get(getPasswordParameter());
password = password != null ? password : "";
//解密
String decryptedPassword = RSAEncrypt.decode(password);
authRequest = new UsernamePasswordAuthenticationToken(username,decryptedPassword);
}catch (IOException e){
e.printStackTrace();
authRequest = new UsernamePasswordAuthenticationToken("","");
}finally {
setDetails(request,authRequest);
//调用authenticate完成认证
return this.getAuthenticationManager().authenticate(authRequest);
}
}
}
//不是json格式按照表单登录逻辑处理
return super.attemptAuthentication(request, response);
}
}
- 自定义实现类UserDetailsServiceImpl,用于查询数据库验证用户
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Set;
/**
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private IUserService userService;
@Autowired
private IRoleService roleService;
@Autowired
private IPermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUserName,username);
wrapper.eq(User::getDelFlag, UserStatus.OK);
User user = userService.getOne(wrapper);
if(StringUtils.isNull(user)){
//throw new UsernameNotFoundException("账号不存在");
throw new ServiceException("账号不存在");
}
UsernameContextHolder.setContext(username);
//查询角色
List<Role> roles = roleService.queryRolesByUser(user.getUserId());
//查询权限
Set<String> permissions = permissionService.queryPermissionByRole(roles);
return new LoginUser().setUserId(user.getUserId()).setUser(user).setRoles(roles).setPermissions(permissions);
}
}
- LoginUser,自定义的实例
import cn.hutool.core.collection.CollectionUtil;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.*;
/**
需要实现UserDetails
*/
@Data
@Accessors(chain = true)
public class LoginUser implements UserDetails {
private Long userId;
private String token;
// user为普通的用户实体类
private User user;
private List<Role> roles;
private Set<String> permissions;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
if(CollectionUtil.isNotEmpty(roles)){
roles.forEach(role->{
MyGrantedAuthority authority = new MyGrantedAuthority(role);
//redis对SimpleGrantedAuthority反序列化有问题
//GrantedAuthority authority = new SimpleGrantedAuthority(role.getRoleKey());
authorities.add(authority);
});
}
return authorities;
}
public static class MyGrantedAuthority implements GrantedAuthority {
private Role role;
private String roleKey;
@Override
public String getAuthority() {
return Objects.isNull(this.role) ? this.roleKey : "ROLE_"+this.role.getRoleKey();
}
public MyGrantedAuthority(Role role) {
this.role = role;
}
public MyGrantedAuthority(String roleKey) {
this.roleKey = roleKey;
}
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return user.getLockFlag() == FlagStatus.NOT.getCode();
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- jwt token验证
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
*/
@Component
public class JwtAuthenticationToKenFilter extends OncePerRequestFilter {
@Autowired
private TokenService tokenService;
@Autowired
private RedisService redisService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = tokenService.getRequestToken(request);
//验证token是否有效
LoginUser loginUser = tokenService.getUserByToken(token);
if(StringUtils.isNotNull(loginUser)){
//判断token是否需要续期
Long expireTime = redisService.getExpire(CacheConstants.LOGIN_USER_TOKEN_KEY + loginUser.getUserId());
if(expireTime>0 && expireTime<=600L){
tokenService.refreshToken(loginUser);
}
}
if(StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())){
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request,response);
}
}
- token的管理
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Component
@ConfigurationProperties(prefix = "jwt")
@Data
@Slf4j
public class TokenService {
// 令牌自定义标识
private String header;
// 令牌秘钥
private String secret;
// 令牌过期时间
private Long expireTime;
@Autowired
private RedisService redisService;
/**
* 创建令牌
*
* @param loginUser
* @return
*/
public String createToken(LoginUser loginUser) {
String token = UUID.randomUUID().toString();
loginUser.setToken(token);
refreshToken(loginUser);
Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token);
return createToken(claims);
}
/**
* 从数据声明生成令牌
*
* @param claims
* @return
*/
private String createToken(Map<String, Object> claims) {
/*return Jwts.builder()
.setClaims(claims)
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
.setSubject("jacob-user")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256, secret)
.compact();*/
return Jwts.builder()
.claims(claims)
.subject("jacob-user")
.issuedAt(new Date())
.signWith(Keys.hmacShaKeyFor(secret.getBytes()), Jwts.SIG.HS256)
.compact();
}
/**
* 刷新令牌有效期
*
* @param loginUser 登录信息
*/
public void refreshToken(LoginUser loginUser) {
//缓存(token,user)
redisService.setCacheObject(getTokenKey(loginUser.getToken()), loginUser, expireTime, TimeUnit.MINUTES);
//缓存(user,token)
redisService.setCacheObject(getUserTokenKey(loginUser), loginUser.getToken(), expireTime, TimeUnit.MINUTES);
}
/**
* 删除用户身份信息
*/
public void deleteToken(LoginUser loginUser) {
if (StringUtils.isNotEmpty(loginUser.getToken())) {
redisService.deleteObject(getTokenKey(loginUser.getToken()));
redisService.deleteObject(getUserTokenKey(loginUser));
}
}
/**
* 删除用户身份信息
*/
public void deleteUserToken(LoginUser loginUser) {
if(redisService.hasKey(getUserTokenKey(loginUser))){
String token = redisService.getCacheObject(getUserTokenKey(loginUser));
redisService.deleteObject(getTokenKey(token));
redisService.deleteObject(getUserTokenKey(loginUser));
}
}
private String getTokenKey(String uuid) {
return CacheConstants.LOGIN_TOKEN_KEY + uuid;
}
private String getUserTokenKey(LoginUser loginUser) {
return CacheConstants.LOGIN_USER_TOKEN_KEY + loginUser.getUserId();
}
/**
* 获取请求中的token
*
* @param request
* @return
*/
public String getRequestToken(HttpServletRequest request) {
String token = request.getHeader(header);
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
token = token.replace(Constants.TOKEN_PREFIX, "");
}
return token;
}
/**
* 获取用户信息
*
* @param token
* @return
*/
public LoginUser getUserByToken(String token) {
if (StringUtils.isEmpty(token)) {
return null;
}
try {
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = claims.get(Constants.LOGIN_USER_KEY, String.class);
return redisService.getCacheObject(getTokenKey(uuid));
} catch (Exception e) {
log.error("验证token失败!{}", e.getMessage());
}
return null;
}
/**
* 从令牌中获取数据声明
*
* @param token
* @return
*/
private Claims parseToken(String token) {
/*return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();*/
return Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(secret.getBytes()))
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
*
* 清除锁定缓存
* @param username
*/
public void clearLoginRecordCache(String username){
if (redisService.hasKey(getPasswordErrorKey(username))) {
redisService.deleteObject(getPasswordErrorKey(username));
}
}
private String getPasswordErrorKey(String username){
return CacheConstants.PWD_ERR_CNT_KEY + username;
}
}
- 成功通知类CustomizeAuthenticationSuccessHandler
import cn.hutool.extra.servlet.JakartaServletUtil;
import eu.bitwalker.useragentutils.UserAgent;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* 登录成功处理
*/
@Component
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private TokenService tokenService;
@Autowired
private IUserService userService;
@Autowired
private SystemConfig systemConfig;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
//登录日志
Thread.ofVirtual().start(()->{
AsyncFactory.recordLogininfor(loginUser.getUsername(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"),
UserAgent.parseUserAgentString(request.getHeader("User-Agent")), JakartaServletUtil.getClientIP(request));
});
//更新登录信息
User user = loginUser.getUser();
user.setLoginTime(System.currentTimeMillis());
userService.updateById(user);
//单点登录
if (systemConfig.getOnlyOneLogin()) {
tokenService.deleteUserToken(loginUser);
}
//删除锁定缓存
tokenService.clearLoginRecordCache(user.getUserName());
//生成token
String token = tokenService.createToken(loginUser);
ResponseUtil.responseToken(response,token);
}
}
小结
整体内容比较简单,主要用于给新手理一下思路,更快的学习使用。