Spring Security 入门配置
>Spring Security 是Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。 一般Web应用的需要进行认证和授权。认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户 授权:经过认证后判断当前用户是否有权限进行某个操作
1. 依赖安装
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring-boot-starter-security.version}</version>
</dependency>
</dependencies>
引入依赖后我们在尝试去访问之前的接口就会自动跳转到一个SpringSecurity的默认登陆页面,默认用户名是user,密码会输出在控制台。必须登陆之后才能对接口进行访问。
2. 认证
2.1 登陆校验流程
2.2 流程
2.2.1 SpringSecurity完整流程
SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。
图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。
UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。
ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
FilterSecurityInterceptor:负责权限校验的过滤器。
2.2.2 认证流程详解
概念速查:
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口:定义了认证Authentication的方法
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
3. 基于Jwt Token登录认证
3.1 自定义返回值
package com.gt.lsv.common.api;
/**
* @Description 通用api返回接口
* @Author ChenJG
* @create 2022/5/16 10:12
*/
public interface IResultCode {
/**
* 返回码
*/
long getCode();
/**
* 返回信息
*/
String getMessage();
}
/**
* @Description 返回结果代码
* @Author ChenJG
* @create 2022/5/16 10:10
*/
public enum ResultCode implements IResultCode {
SUCCESS(0, "操作成功"),
FAILED(500, "操作失败"),
VALIDATE_FAILED(404, "参数检验失败"),
UNAUTHORIZED(401, "认证失败"),
FORBIDDEN(403, "没有相关权限");
private long code;
private String message;
ResultCode(long code, String message) {
this.code = code;
this.message = message;
}
public long getCode() {
return code;
}
public String getMessage() {
return message;
}
}
/**
* @Description 通用返回结果
* @Author ChenJG
* @create 2022/5/16 10:09
*/
public class CommonResult<T> {
/**
* 状态码
*/
private long code;
/**
* 提示信息
*/
private String message;
/**
* 数据封装
*/
private T data;
protected CommonResult() {
}
protected CommonResult(long code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
/**
* 成功返回结果
*
* @param data 获取的数据
*/
public static <T> CommonResult<T> success(T data) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
/**
* 成功返回结果
*
* @param data 获取的数据
* @param message 提示信息
*/
public static <T> CommonResult<T> success(T data, String message) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data);
}
/**
* 失败返回结果
*
* @param errorCode 错误码
*/
public static <T> CommonResult<T> failed(IResultCode errorCode) {
return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), null);
}
/**
* 失败返回结果
*
* @param errorCode 错误码
* @param message 错误信息
*/
public static <T> CommonResult<T> failed(IResultCode errorCode, String message) {
return new CommonResult<T>(errorCode.getCode(), message, null);
}
/**
* 失败返回结果
*
* @param message 提示信息
*/
public static <T> CommonResult<T> failed(String message) {
return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null);
}
/**
* 失败返回结果
*/
public static <T> CommonResult<T> failed() {
return failed(ResultCode.FAILED);
}
/**
* 参数验证失败返回结果
*/
public static <T> CommonResult<T> validateFailed() {
return failed(ResultCode.VALIDATE_FAILED);
}
/**
* 参数验证失败返回结果
*
* @param message 提示信息
*/
public static <T> CommonResult<T> validateFailed(String message) {
return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null);
}
/**
* 未登录返回结果
*/
public static <T> CommonResult<T> unauthorized(T data) {
return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
}
/**
* 未授权返回结果
*/
public static <T> CommonResult<T> forbidden(T data) {
return new CommonResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data);
}
public long getCode() {
return code;
}
public void setCode(long code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
3.2 Jwt生成工具
引入依赖
<!--JWT(Json Web Token)登录支持-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
application.yml token头
jwt:
tokenHeader: Authorization #JWT存储的请求头
secret: mall-portal-secret #JWT加解密使用的密钥
expiration: 604800 #JWT的超期限时间(60*60*24*7)
tokenHead: 'Bearer ' #JWT负载中拿到开头
package com.gt.lsv.security.util;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @Description JwtToken生成工具类
* @Author ChenJG
* @create 2022/5/14 21:41
*/
@Component
public class JwtTokenUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
@Value("${jwt.tokenHead}")
private String tokenHead;
/**
* 根据负责生成JWT的token
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 从token中获取JWT中的负载
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
LOGGER.info("JWT格式验证失败:{}", token);
}
return claims;
}
/**
* 生成token的过期时间
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
/**
* 从token中获取登录用户名
*/
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 验证token是否还有效
*
* @param token 客户端传入的token
* @param userDetails 从数据库中查询出来的用户信息
*/
public boolean validateToken(String token, UserDetails userDetails) {
String username = getUserNameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 判断token是否已经失效
*/
private boolean isTokenExpired(String token) {
Date expiredDate = getExpiredDateFromToken(token);
return expiredDate.before(new Date());
}
/**
* 从token中获取过期时间
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
/**
* 根据用户信息生成token
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 当原来的token没过期时是可以刷新的
*
* @param oldToken 带tokenHead的token
*/
public String refreshHeadToken(String oldToken) {
if (StrUtil.isEmpty(oldToken)) {
return null;
}
String token = oldToken.substring(tokenHead.length());
if (StrUtil.isEmpty(token)) {
return null;
}
//token校验不通过
Claims claims = getClaimsFromToken(token);
if (claims == null) {
return null;
}
//如果token已经过期,不支持刷新
if (isTokenExpired(token)) {
return null;
}
//如果token在30分钟之内刚刷新过,返回原token
if (tokenRefreshJustBefore(token, 30 * 60)) {
return token;
} else {
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
}
/**
* 判断token在指定时间内是否刚刚刷新过
*
* @param token 原token
* @param time 指定时间(秒)
*/
private boolean tokenRefreshJustBefore(String token, int time) {
Claims claims = getClaimsFromToken(token);
Date created = claims.get(CLAIM_KEY_CREATED, Date.class);
Date refreshDate = new Date();
//刷新时间在创建时间的指定时间内
if (refreshDate.after(created) && refreshDate.before(DateUtil.offsetSecond(created, time))) {
return true;
}
return false;
}
}
3.3 Jwt过滤器
package com.gt.lsv.security.commponent;
import com.gt.lsv.common.service.RedisService;
import com.gt.lsv.security.util.JwtTokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Description Jwt过滤器 判断token是否存在再放行
* @Author ChenJG
* @create 2022/5/25 11:08
*/
//保证请求只过滤一次
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private RedisService redisService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String authHeader = request.getHeader(this.tokenHeader);
if (StringUtils.hasText(authHeader) && authHeader.startsWith(this.tokenHead)) {
String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
String username = jwtTokenUtil.getUserNameFromToken(authToken);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 从redis从获取
UserDetails userDetails = (UserDetails) this.redisService.get(username);
if (userDetails != null && jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
LOGGER.info("authenticated user:{}", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(request, response);
}
}
3.4 SecurittConfig
package com.gt.lsv.security.config;
import com.gt.lsv.security.commponent.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @Description SEC 配置类
* @Author ChenJG
* @create 2022/5/23 16:06
*/
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
// 自定义的password编码
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private AccessDeniedHandler accessDeniedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable().httpBasic()
.and()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/user/register", "/user/login","/user/captcha","/user/forget").anonymous()
.antMatchers(HttpMethod.GET, // 允许对于网站静态资源的无授权访问
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/**/*.jpg",
"/swagger-resources/**",
"/v2/api-docs/**"
)
.permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
//自定义过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
http.exceptionHandling()
//配置认证失败
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
//允许跨域
http.cors();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
3.4 实现UserDetails接口
package com.gt.lsv.portal.domain;
import com.alibaba.fastjson.annotation.JSONField;
import com.gt.lsv.model.UmsUser;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @Description 用户详情封装,该接口用于存储用户账号密码权限信息
* @Author ChenJG
* @create 2022/6/1 21:39
*/
@Data
@NoArgsConstructor
@Component
public class LsvUserDetails implements UserDetails {
private UmsUser umsUser;
@JSONField(serialize = false)
private Set<SimpleGrantedAuthority> simpleGrantedAuthorities;
public LsvUserDetails(UmsUser user) {
this.umsUser = user;
}
public LsvUserDetails(UmsUser user, List<String> permissions) {
this.umsUser = user;
//TODO 存在redis 可能无法序列化
simpleGrantedAuthorities = new HashSet<>();
permissions.forEach(permission -> {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
simpleGrantedAuthorities.add(simpleGrantedAuthority);
});
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return umsUser.getPassword();
}
@Override
public String getUsername() {
return umsUser.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;
}
}
3.5 实现UserDetailsService接口
package com.gt.lsv.portal.domain;
import com.gt.lsv.model.UmsUser;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import java.util.Objects;
/**
* @Description 实现该接口后 SpringSecurity 会调用该方法进行认证
* @Author ChenJG
* @create 2022/6/11 19:34
*/
public class LsvUserService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String userName) {
UmsUser user = getUserByUserName(userName);
if (Objects.isNull(user))
throw new UsernameNotFoundException("用户不存在");
return new LsvUserDetails(user);
}
}
3.6 UserService 调用认证
package com.gt.lsv.portal.service.impl;
import com.gt.lsv.common.api.CommonResult;
import com.gt.lsv.common.service.RedisService;
import com.gt.lsv.mapper.UmsUserMapper;
import com.gt.lsv.model.UmsUser;
import com.gt.lsv.model.UmsUserExample;
import com.gt.lsv.portal.domain.LsvUserDetails;
import com.gt.lsv.portal.service.UmsUserService;
import com.gt.lsv.portal.util.InviteCodeUtil;
import com.gt.lsv.portal.util.MailUtil;
import com.gt.lsv.portal.util.StringUtil;
import com.gt.lsv.security.util.JwtTokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* @Description 用户相关操作实现
* @Author ChenJG
* @create 2022/6/1 21:32
*/
@Service
public class UmsUserServiceImpl implements UmsUserService {
private static final Logger LOGGER = LoggerFactory.getLogger(UmsUserServiceImpl.class);
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Value("${redis.key.prefix.captchaRegister}")
private String captchaRegisterPrefix;
@Value("${redis.key.prefix.captchaForgetPassword}")
private String captchaForgetPasswordPrefix;
@Value("${redis.key.expire.captcha}")
private int captchaExpire;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private RedisService redisService;
@Autowired
UmsUserMapper umsUserMapper;
@Override
public CommonResult login(String userName, String password) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userName, password);
// 认证 会调用UserDetails中的验证方法
Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
//AuthencitionaManger 认证通过
if (Objects.isNull(authentication)) {
throw new RuntimeException("认证失败");
}
LsvUserDetails userDetails = (LsvUserDetails) authentication.getPrincipal();
String token = jwtTokenUtil.generateToken(userDetails);
Map<String, String> tokenMap = new IdentityHashMap<>();
tokenMap.put("token", token);
tokenMap.put("tokenHead", tokenHead);
redisService.set(userDetails.getUmsUser().getUserName(), userDetails);
//通过
return CommonResult.success(tokenMap, "登录成功");
}
@Override
public UserDetails loadUserByUsername(String userName) {
UmsUser user = getUserByUserName(userName);
if (Objects.isNull(user))
throw new UsernameNotFoundException("用户不存在");
// List<String> list = new ArrayList<>(Arrays.asList("test", "admin"));
return new LsvUserDetails(user);
}
/**
* 根据id获取user
*
* @param userName
* @return
*/
private UmsUser getUserByUserName(String userName) {
UmsUser umsUser = umsUserMapper.selectByPrimaryKey(userName);
return umsUser;
}
}
4. 授权
4.1 授权基本流程
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。然后设置我们的资源所需要的权限即可。
4.2 授权实现
4.2.1 限制访问资源所需权限
SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。但是要使用它我们需要先开启相关配置。
@EnableGlobalMethodSecurity(prePostEnabled = true)
然后就可以使用对应的注解。@PreAuthorize
@RestController
public class HelloController {
@RequestMapping("/hello")
@PreAuthorize("hasAuthority('test')")
public String hello(){
return "hello";
}
}
4.2.2 封装权限信息
将权限封装在UserDetails
package com.gt.lsv.portal.domain;
import com.alibaba.fastjson.annotation.JSONField;
import com.gt.lsv.model.UmsUser;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @Description 用户详情封装
* @Author ChenJG
* @create 2022/6/1 21:39
*/
@Data
@NoArgsConstructor
@Component
public class LsvUserDetails implements UserDetails {
private UmsUser umsUser;
@JSONField(serialize = false)
private Set<SimpleGrantedAuthority> simpleGrantedAuthorities;
public LsvUserDetails(UmsUser user) {
this.umsUser = user;
}
public LsvUserDetails(UmsUser user, List<String> permissions) {
this.umsUser = user;
//TODO 存在redis 可能无法序列化
simpleGrantedAuthorities = new HashSet<>();
permissions.forEach(permission -> {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
simpleGrantedAuthorities.add(simpleGrantedAuthority);
});
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return simpleGrantedAuthority;
}
@Override
public String getPassword() {
return umsUser.getPassword();
}
@Override
public String getUsername() {
return umsUser.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;
}
}
4.2.3 RBAC权限模型
RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。我们可以把权限存入数据库,在生成details的时候存入权限信息。
5. 自定义失败处理
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
5.1 自定义认证失败
package com.gt.lsv.security.handler;
import com.alibaba.fastjson.JSON;
import com.gt.lsv.common.api.CommonResult;
import com.gt.lsv.security.util.WebUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Description 自定义认证失败
* @Author ChenJG
* @create 2022/5/28 20:38
*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//处理异常 或者授权失败
String str= JSON.toJSONString(CommonResult.unauthorized(authException.getMessage()));
WebUtil.renderString(response,str);
}
}
5.2 自定义授权失败
package com.gt.lsv.security.handler;
import com.alibaba.fastjson.JSON;
import com.gt.lsv.common.api.CommonResult;
import com.gt.lsv.security.util.WebUtil;
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;
/**
* @Description 授权失败
* @Author ChenJG
* @create 2022/5/28 20:52
*/
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//处理异常 或者授权失败
String str = JSON.toJSONString(CommonResult.forbidden(accessDeniedException.getMessage()));
WebUtil.renderString(response, str);
}
}
5.3 在Serurity Config添加过滤器
http.exceptionHandling()
//配置认证失败
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
6. 跨域
浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。 前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。
6.1 Spring boot 跨域配置
package com.gt.lsv.security.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @Description 跨域配置
* @Author ChenJG
* @create 2022/5/29 12:57
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowCredentials(true)
//请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
//请求头
.allowedHeaders("*")
//允许跨域时间
.maxAge(3600);
WebMvcConfigurer.super.addCorsMappings(registry);
}
}
6.2 Security Config允许跨域
//允许跨域
http.cors();
6.3 CSRF
CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。
https://blog.csdn.net/freeking101/article/details/86537087
SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。
我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。