Spring Security 初体验
①认证过滤器(登录)
用于接收前端用户登录信息(username和password)与数据库用户信息(通过UserDetailsService查询)就行判断。
UserDetailsService:查询存在用户信息返回SecurityUser对象,否则抛出异常。
JWTPasswordHandler:判断密码是否正确。
/**
* @author XS
* @Version v1.0
* @ClassName: 认证过滤器
* @Description:
* @Date: 2022/2/1 18:44
*/
@Slf4j
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private RedisTemplate redisTemplate;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager, RedisTemplate redisTemplate) {
this.authenticationManager = authenticationManager;
this.redisTemplate = redisTemplate;
this.setPostOnly(false);
//设置登录url和请求方式
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/member/rbac/login", "POST"));
}
/**
* @description: 接收认证信息
* @author: XS
* @date: 2022/2/13
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
//前端用户登录信息
User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* @description: 登录成功
* @author: XS
* @date: 2022/2/10
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//获取安全框架用户(UserDetailsServiceImpl查到的)
SecurityUser securityUser = (SecurityUser) authResult.getPrincipal();
//生成token
String token = JWTTokenHandler.createToken(securityUser.getCurrentUserInfo().getId(), securityUser.getCurrentUserInfo().getUsername(), securityUser.getPermissionValueList());
log.info("login success,Token:" + token);
//返回结果给前端
HashMap<String, String> login = new HashMap<>();
login.put("token", token);
Result<HashMap<String, String>> ok = Result.OK(login);
response.setContentType("text/html;charset=utf-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(ok));
//存入redis
redisTemplate.opsForValue().set(securityUser.getCurrentUserInfo().getUsername(), token);
}
/**
* @description: 登录失败
* @author: XS
* @date: 2022/2/10
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
log.info("login failure,Error:" + failed.toString());
Result<Object> error = Result.error("登录失败");
response.setContentType("text/html;charset=utf-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(error));
}
}
②授权过滤器(分配角色)
每次请求,判断是否带有token,如果存在则根据token得到用户角色,并在全局安全框架上下文设置用户角色。不存在则直接进入过滤(没角色)。
/**
* @author XS
* @Version v1.0
* @ClassName: 授权过滤器
* @Description:
* @Date: 2022/2/1 18:44
*/
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
logger.info("request uri:" + request.getRequestURI());
String token = request.getHeader(SecurityConstants.TOKEN_HEADER);
// 如果请求头中没有Authorization信息则直接放行了
if (StringUtils.isEmpty(token)) {
logger.warn("X-Token: not exist!");
SecurityContextHolder.clearContext();
chain.doFilter(request, response);
return;
}
logger.info("X-Token:" + token);
// 如果请求头中有token,则进行解析,并且设置授权信息(设置角色)
//请求头中有 token 并且 token 的格式正确,则进行解析并判断 token 的有效性,然后会在 Spring Security 全局设置授权信息
SecurityContextHolder.getContext().setAuthentication(getAuthentication(token));
System.out.println(SecurityContextHolder.getContext());
super.doFilterInternal(request, response, chain);
}
private UsernamePasswordAuthenticationToken getAuthentication(String token) {
try {
// 通过 token 获取用户具有的角色
String username = JWTTokenHandler.getUsernameByToken(token);
List<String> roles = JWTTokenHandler.getRolesByToken(token);
//封装List<SimpleGrantedAuthority>
if (Objects.requireNonNull(roles).size() > 0) {
List<SimpleGrantedAuthority> authorities = roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return new UsernamePasswordAuthenticationToken(username, token, authorities);
}
} catch (ExpiredJwtException exception) {
logger.error("Request to parse JWT with invalid signature . Detail : " + exception.getMessage());
}
return null;
}
}
③实体类
SecurityUser:安全框架实体类
User:前端用户登录信息类
/**
* @author XS
* @Version v1.0
* @ClassName: SecurityUser
* @Description:
* @Date: 2022/2/1 16:13
*/
@Data
@Slf4j
public class SecurityUser implements UserDetails {
//当前登录用户
private transient User currentUserInfo;
//当前权限
private List<String> permissionValueList;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
@Override
public String getUsername() {
return currentUserInfo.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
/**
* @description: 前端用户登录信息
* @author: XS
* @date: 2022/2/17
*/
@Data
@ApiModel(description = "用户实体类")
public class User implements Serializable {
private String id;
private String username; // 不要改属性名
private String password; // 不要改属性名
private String salt;
private String token;
}
④处理类
JWTLogoutHandler:登出处理类(登出成功后处理)
JWTPasswordHandler:密码处理类(密码加密和密码比较)
JWTTokenHandler:token处理类(生成token和通过token获取用户信息)
/**
* @author XS
* @Version v1.0
* @ClassName: LogoutHandler
* @Description:
* @Date: 2022/2/1 16:07
*/
@Slf4j
public class JWTLogoutHandler implements LogoutSuccessHandler {
private RedisTemplate redisTemplate;
public JWTLogoutHandler(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String token = request.getHeader(SecurityConstants.TOKEN_HEADER);
if (token != null) {
//清空当前用户缓存中的权限数据
String username = JWTTokenHandler.getUsernameByToken(token);
redisTemplate.delete(username);
}
log.info("logout success");
Result<String> logout = Result.OK("logout success");
response.setContentType("text/html;charset=utf-8");
try {
response.getWriter().write(new ObjectMapper().writeValueAsString(logout));
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* @author XS
* @Version v1.0
* @ClassName: PasswordHandler
* @Description:
* @Date: 2022/2/1 15:31
*/
@Slf4j
public class JWTPasswordHandler implements PasswordEncoder {
@Override
public String encode(CharSequence originalPassword) { // 密码加密
return SecureUtil.md5(originalPassword.toString());
}
@Override
public boolean matches(CharSequence originalPassword, String encodedPassword) { // 密码加密比较
return originalPassword.equals(encodedPassword);
//return SecureUtil.md5(originalPassword.toString()).equals(encodedPassword); //加密后比较目前没加密
}
}
/**
* @author ASUS
* @Version v1.0
* @ClassName: JWTTokenHandler
* @Description:
* @Date: 2022/2/1 16:00
*/
public class JWTTokenHandler {
public static String createToken(String id, String username, List<String> roles) {
String JwtToken = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
.setSubject("zqy-user")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + SecurityConstants.EXPIRE))
.claim("id", id)
.claim("username", username)
.claim("roles", roles)
.signWith(SignatureAlgorithm.HS256, SecurityConstants.JWT_SECRET_KEY)
.compact();
return JwtToken;
}
/**
* 根据token获取用户Username
*
* @param token
* @return
*/
public static String getUsernameByToken(String token) {
if (StringUtils.isEmpty(token)) return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(SecurityConstants.JWT_SECRET_KEY).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
return (String) claims.get("username");
}
/**
* 根据token获取用户id
*
* @param jwtToken
* @return
*/
public static String getUserIdByToken(String jwtToken) {
if (StringUtils.isEmpty(jwtToken)) return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(SecurityConstants.JWT_SECRET_KEY).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody();
return (String) claims.get("id");
}
/**
* 根据token获取用户roles
*
* @param jwtToken
* @return
*/
public static List<String> getRolesByToken(String jwtToken) {
if (StringUtils.isEmpty(jwtToken)) return null;
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(SecurityConstants.JWT_SECRET_KEY).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody();
return (List<String>) claims.get("roles");
}
}
⑤认证详情服务类(数据库查询用户信息)
/**
* @description: 认证详情(数据库查询)
* @author: XS
* @date: 2022/2/17
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private SysUserService sysUserService;
@Resource
private SysRoleService sysRoleService;
@Resource
private UserRoleService userRoleService;
/***
* 根据账号获取用户信息
* @param username:
* @return: org.springframework.security.core.userdetails.UserDetails
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库中取出用户信息
SysUser sysUser = sysUserService.selectByUsername(username);
// 判断用户是否存在
if (sysUser == null) {
throw new UsernameNotFoundException("用户不存在!");
}
// 当前用户
User curUser = new User();
BeanUtils.copyProperties(sysUser, curUser);
//查询用户角色
List<UserRole> userRoleList = userRoleService.getUserRoleByUserId(curUser.getId());
List<String> roleIdList = userRoleList.stream().map(UserRole::getRoleId).collect(Collectors.toList());
List<SysRole> sysRoles = sysRoleService.listByIds(roleIdList);
List<String> authorities = sysRoles.stream().map(sysRole -> "ROLE_" + sysRole.getRoleCode()).collect(Collectors.toList());
SecurityUser securityUser = new SecurityUser();
//设置当前用户
securityUser.setCurrentUserInfo(curUser);
//设置当前权限(角色)
securityUser.setPermissionValueList(authorities);
return securityUser;
}
}
⑥常量类
/**
* @author XS
* @description Spring Security相关配置常量
*/
public final class SecurityConstants {
/**
* @description: token过期时间
* @author: XS
* @date: 2022/2/13
*/
public static final long EXPIRE = 1000 * 60 * 60 * 24; //存在时间
/**
* rememberMe 为 false 的时候过期时间是1个小时
*/
public static final long EXPIRATION = 60 * 60L;
/**
* rememberMe 为 true 的时候过期时间是7天
*/
public static final long EXPIRATION_REMEMBER = 60 * 60 * 24 * 7L;
/**
* JWT签名密钥硬编码到应用程序代码中,应该存放在环境变量或.properties文件中。
*/
public static final String JWT_SECRET_KEY = "C*F-JaNdRgUkXn2r5u8x/A?D(G+KbPeShVmYq3s6v9y$B&E)H@McQfTjWnZr4u7w";
// 前端token头
public static final String TOKEN_HEADER = "X-Token";
// 资源白名单
public static final String[] RESOURCE_WHITELIST = {
"/swagger-ui.html",
"/swagger-ui/*",
"/swagger-resources/**",
"/v2/api-docs",
"/v3/api-docs",
"/webjars/**",
//knife4j
"/doc.html",
};
public static final String H2_CONSOLE = "/h2-console/**";
// 认证白名单(不用登录也能访问的接口)
public static final String[] AUTHENTICATION_WHITELIST = {
"/member/rbac/login",
"/member/rbac/logout",
"/member/rbac/register",
};
//授权白名单(没有权限也能访问的接口)
public static final String[] AUTHORIZATION_WHITELIST = {
//TODO
};
private SecurityConstants() {
}
}
⑦核心配置类(将上面自定义的类进行配置)
/**
* @author XS
* @Version v1.0
* @ClassName: Web安全框架配件类
* @Description:
* @Date: 2022/2/1 15:28
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetailsService;
@Resource
private RedisTemplate redisTemplate;
/**
* @description: 核心配置
* @author: XS
* @date: 2022/2/14
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
log.info("spring-security started successfully!");
//todo remember me
http.csrf().disable()//关闭csrf
.authorizeRequests()
// 认证白名单(不用认证,不用登录)
.antMatchers(SecurityConstants.AUTHENTICATION_WHITELIST).anonymous()
// 授权白名单(无权限请求)
.antMatchers(SecurityConstants.AUTHORIZATION_WHITELIST).permitAll()
// 权限请求
.anyRequest().authenticated().and()
// 登出路径
.logout().logoutUrl("/member/rbac/logout")
// 登出成功处理器
.logoutSuccessHandler(new JWTLogoutHandler(redisTemplate)).and()
// 认证过滤器
.addFilter(new JWTAuthenticationFilter(authenticationManager(), redisTemplate))
// 授权过滤器
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// 不需要session(不创建会话)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 异常处理
.exceptionHandling()
// 授权异常
.authenticationEntryPoint(new JWTAuthenticationEntryPoint())
// 请求拒绝
.accessDeniedHandler(new JWTAccessDeniedHandler());
}
/**
* 密码处理
*
* @param auth
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new JWTPasswordHandler());
}
/**
* 配置哪些请求不拦截
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(SecurityConstants.RESOURCE_WHITELIST);
}
}