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 登陆校验流程

image-20211215094003288

2.2 流程

2.2.1 SpringSecurity完整流程

SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。

image-20220610194127464

图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。

UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。

ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。

FilterSecurityInterceptor:负责权限校验的过滤器。

2.2.2 认证流程详解

image-20211214151515385

概念速查:

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的时候存入权限信息。

image-20211222110249727

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攻击也就不用担心了。

7. 参考

  1. B站三更草堂 https://www.bilibili.com/video/BV1mm4y1X7Hc?spm_id_from=333.337.top_right_bar_window_custom_collection.content.click
  2. Github Mall 项目 https://github.com/macrozheng/mall
posted @ 2022-06-11 20:38  国-哥  阅读(714)  评论(0编辑  收藏  举报