SpringSecurity Rbac 基础9个类
Bitgeek 2024-02-14 SpringSecuritySpringBootRbac
# 需要实现的基础9个类
| 序号 | 类名 | 作用 |
|---|---|---|
| 1 | AccountUser | 用户实体类,对这个UserDetails类进行扩展里面封装本系统权限与角色 |
| 2 | AccountUserDetailsService | 实现了UserDetailsService 接口 里面的 loadUserByUsername 方法 |
| 3 | JwtAccessDeniedHandler | 实现 AccessDeniedHandler 当授权失败 返回 403的时候 进行业务处理 |
| 4 | JwtAuthenticationEntryPoint | 实现 AuthenticationEntryPoint 用于处理认证失败或者未认证的请求 当用户尝试访问受保护的资源而未经过身份验证,,或者身份验证失败的时候,调用 |
| 5 | JwtAuthenticationFilter | 实现 OncePerRequestFilter是Spring Boot里面的一个过滤器抽象类,其同样在Spring Security里面被广泛用到,这个过滤器抽象类通常被用于继承实现并在每次请求时只执行一次过滤。 |
| 6 | JwtLogoutSuccessHandler | 实现 当用户登出成功后 使用这个接口LogoutSuccessHandler |
| 7 | LoginFailureHandler | 实现 登录失败 进行业务处理 接口话柄 |
| 8 | LoginSuccessHandler | 实现 登录成功 进行业务处理 接口话柄 |
| 9 | JwtUtil | 实现 jwt 的 token工具类 |
# Rbac的代码配置
# AccountUser
package cn.bitgeek.rbac;
import com.google.common.base.Preconditions;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* PROJECT_NAME: springboot-v3-demo
* DESCRIPTION:<p></p>
*
* @author: liqingzhu
* @since: 1.0.0
* @version: 1.0.0
*/
public class AccountUser implements UserDetails {
private static final long serialVersionUID = -1L;
private Integer userId;
private String password;
private String username;
private Collection<? extends GrantedAuthority> authorities;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
public AccountUser(Integer userId, String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(userId, username, password, true, true, true, true, authorities);
}
public AccountUser(Integer userId, String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
// Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor");
Preconditions.checkState(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor");
this.userId = userId;
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
public Integer getUserId() {
return this.userId;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return this.accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return this.accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return this.credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
}
# AccountUserDetailsService
package cn.bitgeek.rbac;
import cn.bitgeek.entity.rbac.User;
import cn.bitgeek.entity.rbac.Permission;
import cn.bitgeek.service.rbac.SysUserService;
import com.google.common.base.Joiner;
import com.qcloud.cos.utils.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.List;
import java.util.stream.Collectors;
/**
* PROJECT_NAME: springboot-v3-demo
* DESCRIPTION:<p></p>
*
* @author: liqingzhu
* @since: 1.0.0
* @version: 1.0.0
*/
@Service
public class AccountUserDetailsService implements UserDetailsService {
@Autowired
private SysUserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.getUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户名或密码错误");
}
return new AccountUser(user.getId(), user.getUsername(), user.getPassword(), getUserAuthority(user.getUsername()));
}
/**
* 获取用户权限信息(角色、菜单权限)
* @param username
* @return
*/
public List<GrantedAuthority> getUserAuthority(String username) {
// 角色(比如ROLE_admin),菜单操作权限(比如sys:user:list)
// 角色必须以ROLE_开头,security在判断角色时会自动截取ROLE_
List<Permission> permissions = userService.getPermissionByUsername(username);
// 比如ROLE_admin,ROLE_normal,sys:user:list,...
String authority = "";
if (!CollectionUtils.isNullOrEmpty(permissions)) {
List<String> urls = permissions.stream().map(Permission::getUrl).collect(Collectors.toList());
authority = Joiner.on(',').join(urls);
}
return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
}
}
# JwtAccessDeniedHandler
package cn.bitgeek.rbac;
import cn.bitgeek.common.ResultDTO;
import cn.bitgeek.util.JsonUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* PROJECT_NAME: springboot-v3-demo
* DESCRIPTION:<p></p>
*
* @author: liqingzhu
* @since: 1.0.0
* @version: 1.0.0
*/
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
ResultDTO resultDTO = ResultDTO.error(e.getMessage());
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
outputStream.write(JsonUtils.toJson(resultDTO).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
# JwtAuthenticationEntryPoint
package cn.bitgeek.rbac;
import cn.bitgeek.common.ResultDTO;
import cn.bitgeek.util.JsonUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* PROJECT_NAME: springboot-v3-demo
* DESCRIPTION:<p></p>
*
* @author: liqingzhu
* @since: 1.0.0
* @version: 1.0.0
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ResultDTO resultDTO = ResultDTO.error("请先登录");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
outputStream.write(JsonUtils.toJson(resultDTO).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
# JwtAuthenticationFilter
package cn.bitgeek.rbac;
import cn.bitgeek.common.ResponseCodeEnum;
import cn.bitgeek.util.BaseException;
import io.jsonwebtoken.Claims;
import jakarta.annotation.Resource;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Resource
private JwtUtil jwtUtil;
@Autowired
private AccountUserDetailsService accountUserDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = request.getHeader(JwtUtil.HEADER);
// 未获取到token,继续往后走,因为后面还有鉴权管理器等去判断是否拥有身份凭证,所以可以放行
// 没有token相当于匿名访问,若有一些接口是需要权限的,则不能访问这些接口
if (!StringUtils.hasLength(token)) {
chain.doFilter(request, response);
return;
}
Claims claims = jwtUtil.getClaimsByToken(token);
if (claims == null) {
throw new BaseException(ResponseCodeEnum.BAD_REQUEST, "token异常");
}
if (jwtUtil.isTokenExpired(claims.getExpiration())) {
throw new BaseException(ResponseCodeEnum.BAD_REQUEST, "token已过期");
}
String username = claims.getSubject();
// 构建UsernamePasswordAuthenticationToken,这里密码为null,是因为提供了正确的token,实现自动登录
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, accountUserDetailsService.getUserAuthority(username));
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
}
# JwtLogoutSuccessHandler
package cn.bitgeek.rbac;
import cn.bitgeek.common.ResultDTO;
import cn.bitgeek.util.JsonUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Component
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
if (authentication != null) {
new SecurityContextLogoutHandler().logout(httpServletRequest, httpServletResponse, authentication);
}
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setHeader(JwtUtil.HEADER, "");
SecurityContextHolder.clearContext();
ResultDTO resultDTO = ResultDTO.success("SuccessLogout");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
outputStream.write(JsonUtils.toJson(resultDTO).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
# LoginFailureHandler
package cn.bitgeek.rbac;
import cn.bitgeek.common.ResultDTO;
import cn.bitgeek.util.JsonUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse httpServletResponse, AuthenticationException exception) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
ResultDTO resultDTO = ResultDTO.error("用户名或密码错误");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
outputStream.write(JsonUtils.toJson(resultDTO).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
# LoginSuccessHandler
package cn.bitgeek.rbac;
import cn.bitgeek.common.ResultDTO;
import cn.bitgeek.util.JsonUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Resource
private JwtUtil jwtUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
// 生成token,并放置到请求头中
String token = jwtUtil.generateToken(authentication.getName());
httpServletResponse.setHeader(JwtUtil.HEADER, token);
ResultDTO resultDTO = ResultDTO.success("SuccessLogin");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
outputStream.write(JsonUtils.toJson(resultDTO).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
# JwtUtil
package cn.bitgeek.rbac;
import com.auth0.jwt.JWT;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Map;
@Component
@Slf4j
public class JwtUtil {
private static final String SECRET = "zxcvbnmfdasaererafafafafafafakjlkjalkfafadffdafadfafafaaafadfadfaf1234567890";
private static final long EXPIRE = 60 * 24 * 7;
public static final String HEADER = "Authorization";
/**
* 生成jwt token
*/
public String generateToken(String username) {
SecretKey signingKey = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));
//过期时间
LocalDateTime tokenExpirationTime = LocalDateTime.now().plusMinutes(EXPIRE);
return Jwts.builder()
.signWith(signingKey, Jwts.SIG.HS512)
.header().add("typ", "JWT").and()
.issuedAt(Timestamp.valueOf(LocalDateTime.now()))
.subject(username)
.expiration(Timestamp.valueOf(tokenExpirationTime))
.claims(Map.of("username", username))
.compact();
}
public Claims getClaimsByToken(String token) {
SecretKey signingKey = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));
return Jwts.parser()
.verifyWith(signingKey)
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 检查token是否过期
*
* @return true:过期
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}
/**
* 获得token中的自定义信息,一般是获取token的username,无需secret解密也能获得
* @param token
* @param filed
* @return
*/
public String getClaimFiled(String token, String filed){
try{
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(filed).asString();
} catch (JWTDecodeException e){
log.error("JwtUtil getClaimFiled error: ", e);
return null;
}
}
public static void main(String[] args) {
JwtUtil jwtUtil = new JwtUtil();
String token = jwtUtil.generateToken("admin");
System.out.println("token = " + token);
Claims claims = jwtUtil.getClaimsByToken(token);
System.out.println("claims = " + claims);
String username = jwtUtil.getClaimFiled(token, "username");
System.out.println("username = " + username);
}
}