• 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
  • 相关阅读:
    MFC之CString操作1
    项目之MFC/VC++疑问巩固1
    赖氏经典英语语法—动词
    赖氏经典英语语法—关系词
    2021.07.08-模型预测轨迹生成
    2021.07.07-基于软约束的轨迹优化-实践
    2021.07.03-基于软约束的轨迹优化-地图
    2021.07.05-基于软约束的轨迹优化-理论
    1.轨迹优化-港科大无人车
    TODO-3-关于无人车贝塞尔曲线
  • 原文地址:https://www.cnblogs.com/dlvguo/p/16366756.html
Copyright © 2020-2023  润新知