当前位置: 首页 > news >正文

SpringBoot整合Spring Security

简介

Spring Security 是 Spring 家族中安全管理框架,相比于 Shiro ,它提供了更丰富的功能,社区资源比 Shiro 丰富。
一般大部分中大型项目都是使用 Security ,小项目使用 Shiro 比较多。因为相对于 Security,Shiro 上手更简单。
一般Web应用的需要进行认证授权
认证:验证是否是本系统用户。
授权:经过登录,判断当前用户拥有的权限。

一、快速入门

  1. pom.xml
       <!--    Security 依赖    -->
       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
  1. controller
/**
 * @Author: Lanys
 * @Description: 测试接口
 * @Date: Create in 22:03 2022/9/20
 */
@RestController
public class AuthController {

    @PostMapping("/hello")
    public String hello() {
        return "Hello Security";
    }
}
  1. 启动在这里插入图片描述
  2. 测试
    账号:user
    密码:项目启动时随机生成
    在这里插入图片描述
    在这里插入图片描述

二、 初探原理

登陆校验流程(前后端分离)

在这里插入图片描述

SpringSecurity完整流程

SpringSecurity的流程其实就是一个过滤链,内包含了多个过滤器。
在这里插入图片描述
图中只展示核心过滤器。
UsernamePasswordAuthenticationFilter:负责处理账号密码登录请求。
ExceptionTranslationFilter: 处理过滤器中抛出的任何AccessDeniedExceptionAuthenticationException
FilterSecurityinterceptor:负责处理权限校验的过滤器。

完整过滤器链

在这里插入图片描述

三、认证

思路

登录:
在这里插入图片描述
校验:
在这里插入图片描述

准备工作

数据表

① mysql 用户数据表

CREATE TABLE `market_user`  (
  `user_id` bigint(0) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
  `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密码',
  `salt` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '盐',
  `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱',
  `mobile` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '手机号',
  `status` tinyint(0) NULL DEFAULT NULL COMMENT '状态  0:禁用   1:正常',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`user_id`) USING BTREE,
  UNIQUE INDEX `username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1406806069329657858 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统用户' ROW_FORMAT = Dynamic;

INSERT INTO `market_user` VALUES (1, 'admin', '$2a$10$nZ5WisHs6gTGFCHNb0iAfum.QaAiOtcXDpLEX.C2/umbPXUuLuuIC', 'i2I9Lr5MKetiO1Zk7IpC', 'root@renren.io', '13612345678', 1, '2016-11-11 11:11:11');

在这里插入图片描述

maven

② xml 依赖配置

<properties>
        <java.version>1.8</java.version>
        <druid.version>1.2.6</druid.version>
        <mysql.version>8.0.22</mysql.version>
        <plus.version>3.4.1</plus.version>
        <swagger.version>2.9.2</swagger.version>
        <druid.version>1.2.6</druid.version>
        <swaggerio.version>1.5.21</swaggerio.version>
        <plus-generator.version>3.4.1</plus-generator.version>
        <swagger-bootstrap>1.9.6</swagger-bootstrap>
        <hutool.varsion>5.4.5</hutool.varsion>
        <jjwt.version>0.9.0</jjwt.version>
        <java-jwt.version>3.2.0</java-jwt.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--    redis    -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--    生菜    -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!-- 阿里巴巴数据库 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>

        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>

        <!-- Mybatis-plus 依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>${plus.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${plus.version}</version>
        </dependency>
        <!--jwt-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>${java-jwt.version}</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${swagger.version}</version>
        </dependency>

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>${swagger-bootstrap}</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.varsion}</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.80</version>
        </dependency>
        <dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
            <version>2.0.1</version>
        </dependency>
    </dependencies>

yml 配置

③ application.yml 配置文件

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/market?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
  mybatis-plus:
    type-aliases-package: com.example.securitydemo.entity
    mapper-locations: classpath*:/mapper/*.xml
    configuration:
      default-statement-timeout: 120
      log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  redis:
    host: 8.134.130.51
    port: 6379
    password: 813413051
#swagger
lanys:
  swagger:
    title: material
    description: material
    termsOfServiceUrl: https://eurasia.plus/swagger-ui.html
    ContactName: xxx
    ContactUrl: https://eurasia.plus/swagger-ui.html
    ContactEmail: 1090613735@qq.com
    version: 1.0

swagger配置

④ swaggerConfig Swagger API配置

/**
 * @Author: Lanys
 * @Description:
 * @Date: Create in 17:14 2021/3/22
 */

@Configuration
@EnableSwagger2
@EnableSwaggerBootstrapUI
     public class SwaggerConfig {

     @Bean
     @ConditionalOnMissingBean
     public SwaggerProperties swaggerProperties(){
          return new SwaggerProperties();
     }


     @Bean
     public Docket createRestApi(SwaggerProperties swaggerProperties){
          return new Docket(DocumentationType.SWAGGER_2)
                  .apiInfo(apiInfo(swaggerProperties))
                  .enable(true)
                  .select()
                  .apis(RequestHandlerSelectors.basePackage("com.example.securitydemo.controller"))
                  .paths(PathSelectors.any())
                  .build();
     }

     /**
      * 创建该API的基本信息(这些基本信息会展现在文档页面中)
      * 访问地址:http://项目实际地址/doc.html
      *
      * @return
      */
     private ApiInfo apiInfo(SwaggerProperties swaggerProperties) {

          Contact contact = new Contact(swaggerProperties.getContactName(), swaggerProperties.getContactUrl(),swaggerProperties.getContactEmail());

          return new ApiInfoBuilder()
                  .title(swaggerProperties.getTitle())
                  //描述
                  .description(swaggerProperties.getDescription())
                  .termsOfServiceUrl(swaggerProperties.getTermsOfServiceUrl())
                  .contact(contact)
                  .version(swaggerProperties.getVersion())
                  .build();
     }
}

⑤ Swagger yml映射

/**
 * @ClassName SwaggerProperties
 * @Author zwy
 * @Data 1/4/2021 下午5:09
 */
@Data
@ConfigurationProperties("lanys.swagger")
public class SwaggerProperties {

    private String title;

    private String description;

    private String termsOfServiceUrl;

    private String ContactName;

    private String ContactUrl;

    private String ContactEmail;

    private String version;
}

Redis 配置

⑥ RedisConfig Redis配置

/**
 * @author lanys
 * @Description: Redis 配置
 * @date 28/6/2021 上午10:43
 */
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisConfig {

    /**
     * redisTemplate模板
     * @param factory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory factory){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);

        // 使用Jackson进行序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        // 设置可见性
        objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 简单的字符串序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}
/**
 * @Author: Lanys
 * @Description: Redis常量
 * @Date: Create in 22:34 2022/5/23
 */
public class RedisConstants {

    /**
     * redis token time
     */
    public static final Long TOKEN_OVERDUE_TIME = 86400L;

    /**
     * Redis User catalog
     */
    public static final String USER_CATALOG = "market:user:{}";


}

/**
 * @Author: Lanys
 * @Description: redis 工具
 * @Date: Create in 21:33 2022/5/22
 */
@Slf4j
@Component
public class RedisUtils {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 写入缓存
     * @param key
     * @param value
     * @return
     */
    public boolean set(final String key, Object value) {
        boolean result = false;
        try {
            ValueOperations<String, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 写入缓存设置时间
     * @param key key
     * @param value value
     * @param expireTime 设置过期时间
     * @return
     */
    public boolean set(final String key,Object value,Long expireTime) {
        boolean result = false;
        try {
            String toJSONString = JSON.toJSONString(value);
            ValueOperations<String, Object> operations = redisTemplate.opsForValue();
            operations.set(key,toJSONString,expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 批量删除数据
     * @param keys key
     */
    public void remove(final String... keys){
        log.info("redis 删除多个数据->{}", (Object) keys);
        for (String key: keys) {
            redisTemplate.delete(key);
        }
    }

    /**
     * 删除单个数据
     * @param key key
     */
    public void remove(final String key) {
        log.info("redis 删除单个数据->{}",key);
        if (exists(key)) {
            redisTemplate.delete(key);
        }
    }

    /**
     * 判断当前key是否存储
     * @param key key
     * @return 是否存在
     */
    public boolean exists(final String key){
        try{
            log.info("redis 校验数据是否存在->{}",key);
            Boolean result = redisTemplate.hasKey(key);
            if (result != null) {
                return result;
            }
        }catch (Exception e) {
            log.error("redis 校验数据是否存在数据异常:",e);
        }
        return false;
    }

    /**
     * 获取单个 redis 数据
     * @param key key
     * @return
     */
    public Object get(final String key) {
        Object data = null;
        try {
            log.info("获取Redis数据->{}",key);
            ValueOperations<String,Object> valueOperations = redisTemplate.opsForValue();
            data = valueOperations.get(key);
        }catch (Exception e) {
            log.error("获取Redis数据数据异常:", e);
        }
        return data;
    }

    /**
     * hash存储
     * @param keyOne redis key
     * @param keyTwo hash 存储key
     * @param value  hash 存储value
     * @return
     */
    public boolean addHash(String keyOne,String keyTwo,Object value) {
        try {
            log.info("hash存储目录->{}下的key->{},value->{}",keyOne,keyTwo,value);
            redisTemplate.opsForHash().put(keyOne,keyTwo,value);
            return true;
        }catch (Exception e) {
            log.error("hash存储类型数据异常:",e);
        }
        return false;
    }

    /**
     *  hash批量存储
     * @param key key
     * @param map map 指的是多个,就是批量
     * @return boolean
     */
    public boolean addHashMap(String key, Map<String,Object> map) {
        try {
           log.info("存储hash类型key->{},value->{}",key,map.toString());
           redisTemplate.opsForHash().putAll(key,map);
           return true;
        }catch (Exception e) {
            log.error("存储hash类型数据异常:",e);
        }
        return false;
    }

    /**
     * 获取hash存储的数据
     * @param keyOne key
     * @param keyTwo  key
     * @return
     */
    public Object getMapString(String keyOne,String keyTwo) {
        try{
            log.info("获取hash目录->{}下的key->{}",keyOne,keyTwo);
            Object result = redisTemplate.opsForHash().get(keyOne, keyTwo);
            if (result != null) {
                return result;
            }
        }catch (Exception e){
            log.error("获取hash目录下的key数据异常:",e);
        }
        return null;
    }
}

Token工具

⑦ TokenUtil token工具

/**
 * @ClassName TokenUtil
 * @Author lanys
 * @Data 1/4/2021 下午7:05
 */
@Slf4j
public class TokenUtil {

    /** 盐 */
    public final static String jwtTokenSecret="MARKET";

    /**
     * 生成token
     * @param key 账号
     * @return
     */
    public static String doGenerateToken(String key) {
        final Date createdDate = new Date();
        //Constants.time * 1000
        final Date finishDate = new Date(createdDate.getTime() + 86400);

        return Jwts.builder()
                .setSubject(key)
                .setIssuedAt(createdDate)
//                .setExpiration(finishDate)
                .signWith(SignatureAlgorithm.HS256, jwtTokenSecret)
                .compact();
    }

    /**
     * 获取body
     * @param token token
     * @return
     */
    public static Claims parsingToken(String token) {
        return Jwts.parser().setSigningKey(jwtTokenSecret) .parseClaimsJws(token)
                .getBody();
    }

}

统一返回封装

⑧ Result 返回封装

/**
 * @Author: Lanys
 * @Description: 统一返回格式
 * @Date: Create in 22:50 2021/11/5
 */
@ApiModel(value = "返回值基本信息")
@Data
public class Result {

    /**
     * 状态
     */
    @ApiModelProperty(value = "状态")
    private int code;

    /**
     * 内容
     */
    @ApiModelProperty(value = "内容")
    private Object data;

    /**
     * 描述
     */
    @ApiModelProperty(value = "描述")
    private String msg;

    public Result(int code, String msg, Object data) {
        this.code = code;
        this.data = data;
        this.msg = msg;
    }

    public Result(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public static Result success() {
        return new  Result(ResultEnums.SUCCESS.getCode(), ResultEnums.SUCCESS.getMsg());
    }

    public static  Result success(String msg) {
        return new  Result(ResultEnums.SUCCESS.getCode(), ResultEnums.SUCCESS.getMsg(), msg);
    }

    /**
     * 无参返回值
     *
     * @return
     */
    public static  Result fail() {
        return new  Result(ResultEnums.FAIL.getCode(), ResultEnums.FAIL.getMsg());
    }

    /**
     * 错误有参返回值
     *
     * @return
     */
    public static  Result fail(Object data) {
        return new  Result(ResultEnums.FAIL.getCode(), ResultEnums.FAIL.getMsg(), data);
    }

    /**
     * 异常无参返回值
     *
     * @return
     */
    public static  Result exception() {
        return new  Result(ResultEnums.EXCEPTION.getCode(), ResultEnums.EXCEPTION.getMsg());
    }

    /**
     * 异常有参返回值
     *
     * @return
     */
    public static  Result exception(Object data) {
        return new  Result(ResultEnums.EXCEPTION.getCode(), ResultEnums.EXCEPTION.getMsg(), data);
    }

    public  Result put(Object data) {
        this.data = data;
        return this;
    }

}

⑨ 枚举

/**
 * @Author: Lanys
 * @Description: 统一返回格式泛型
 * @Date: Create in 22:53 2021/11/5
 */
@Getter
public enum ResultEnums {

    SUCCESS(200, "成功"),
    FAIL(500, "异常"),
    EXCEPTION(500, "失败");

    private int code;

    private String msg;

    ResultEnums(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }


}

Security 核心配置

⑩ SecurityConfig Security核心配置

/**
 * @Author: Lanys
 * @Description: Security 配置类
 * @Date: Create in 23:35 2022/8/27
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

     @Resource
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    /**
     * 创建 BCryptPasswordEncoder 注入容器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        System.out.println("加载加密配置");
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 跨域
                .csrf().disable()
                // 不通过Session 获取 SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口,允许访问
                .antMatchers("/login","/doc.html","/swagger/**","/v2/api-docs"
                        ,"/swagger-ui.html","/swagger-resources/**","/webjars/**","/logout").anonymous()
                // 除外都要认证
                .anyRequest().authenticated();
                http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

⑩① UserDetailsServiceImpl 认证Service

/**
 * @Author: Lanys
 * @Description:
 * @Date: Create in 22:35 2022/8/26
 */
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private IMarketUserService marketUserService;

    /**
     * 校验账号密码
     * @param username 账号
     * @return 用户信息
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {
            // 查询用户信息
            LambdaQueryWrapper<MarketUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
            lambdaQueryWrapper.eq(MarketUser::getUsername,username);
            MarketUser marketUser = this.marketUserService.getOne(lambdaQueryWrapper);
            if (marketUser == null) {
                throw  new RuntimeException("用户或密码错误");
            }
            log.info("[Security查询用户信息]参数:-> {}",marketUser.toString());
            return new LoginUser(marketUser);
        }catch (Exception e) {
            log.error("Security校验账号密码异常:",e);
        }
        return null;
    }
}

⑩② LoginUser 重写Security 用户详情

/**
 * @Author: Lanys
 * @Description: 实现 UserDetails
 * @Date: Create in 22:46 2022/8/26
 */
@Setter
@Getter
@NoArgsConstructor
public class LoginUser implements UserDetails, Serializable {

    private MarketUser marketUser;

    public LoginUser(MarketUser marketUser) {
        this.marketUser = marketUser;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return this.marketUser.getPassword();
    }

    @Override
    public String getUsername() {
        return this.marketUser.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;
    }
}

⑩③ JwtAuthenticationTokenFilter token过滤器

/**
 * @Author: Lanys
 * @Description: security token过滤器
 * @Date: Create in 23:02 2022/8/28
 */
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private RedisUtils redisUtils;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        try {
            String authorization = httpServletRequest.getHeader("Authorization");
            // 没有token,放行
            if (StrUtil.isBlank(authorization)) {
                filterChain.doFilter(httpServletRequest,httpServletResponse);
                return;
            };
            // 有token逻辑
            log.info("过滤器校验token:{}",authorization);
            Claims claims = TokenUtil.parsingToken(authorization);
            String subject = claims.getSubject();
            if (StrUtil.isNotBlank(subject)) {
                Object obj = redisUtils.get(StrUtil.format(RedisConstants.USER_CATALOG,subject));
                LoginUser loginUser = null;
                if (obj != null) {
                    String toJSONString = JSON.toJSONString(obj);
                    Object parse = JSON.parse(toJSONString);
                    loginUser = JSONObject.parseObject(parse.toString(), LoginUser.class);
                }
                if (loginUser == null) {
                    throw new RuntimeException("用户未登录");
                }
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                filterChain.doFilter(httpServletRequest,httpServletResponse);
            }

        }catch (Exception e) {
            log.error("过滤器校验token异常:",e);
        }

    }
}

三层

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("market_user")
public class MarketUser implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "user_id", type = IdType.AUTO)
    private Long userId;

    @TableField("username")
    private String username;

    @TableField("password")
    private String password;

    @TableField("salt")
    private String salt;

    @TableField("email")
    private String email;

    @TableField("mobile")
    private String mobile;

    @TableField("status")
    private Integer status;

    @TableField("create_time")
    private LocalDateTime createTime;

}
public interface IMarketUserService extends IService<MarketUser> {}
@Service
public class MarketUserServiceImpl extends ServiceImpl<MarketUserMapper, MarketUser> implements IMarketUserService {}
public interface IMarketUserService extends IService<MarketUser> {}
/**
 * @Author: Lanys
 * @Description: 测试接口
 * @Date: Create in 22:03 2022/9/20
 */
@RestController
public class AuthController {

    @Resource
    private AuthenticationManager authenticationManager;

    @Resource
    private RedisUtils redisUtils;

    @ApiOperation(value = "登录模块")
    @PostMapping("/login")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "userName", value = "账号", paramType = "query", example = "username"),
            @ApiImplicitParam(name = "password", value = "密码", paramType = "query", example = "password")
    })
    public Result login(@NotBlank(message = "账号不能为空") String userName,
                        @NotBlank(message = "密码不能为空") String password) {
        // AuthenticationManager 用户验证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userName,password);
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if (authenticate == null) {
            return Result.fail("账号或密码异常");
        }
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        HashMap<String, Object> hashMap = new HashMap<>(2);

        if (loginUser.getMarketUser() != null) {
            Long userId = loginUser.getMarketUser().getUserId();
            redisUtils.set(StrUtil.format(RedisConstants.USER_CATALOG,userId),loginUser, RedisConstants.TOKEN_OVERDUE_TIME);
            hashMap.put("token", TokenUtil.doGenerateToken(userId.toString()));
        }
        return Result.success("登录成功").put(hashMap);
    }

    @ApiOperation("註銷")
    @PatchMapping("/logout")
    public Result logout () {
        UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authenticationToken.getPrincipal();
        Long userId = loginUser.getMarketUser().getUserId();
        redisUtils.remove(StrUtil.format(RedisConstants.USER_CATALOG,userId));
        return Result.success();
    }

    @GetMapping("/hello")
    public String hello() {
        return "Hello Security";
    }
}

测试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

四、授权

正常情况下,在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
​ 所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。
​ 然后设置我们的资源所需要的权限即可。

授权使用

要使用它我们需要先开启相关配置 SecurityConfig

@EnableGlobalMethodSecurity(prePostEnabled = true)

然后就可以使用对应的注解。@PreAuthorize
sys:schedule:list,sys:schedule:info 指权限

    @PreAuthorize("hasAnyAuthority('sys:schedule:list,sys:schedule:info')")
    @GetMapping("/hello")
    public String hello() {
        return "Hello Security";
    }

准备工作

① mysql


DROP TABLE IF EXISTS `sys_menu`;

CREATE TABLE `sys_menu` (
  `menu_id` bigint NOT NULL AUTO_INCREMENT,
  `parent_id` bigint DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',
  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '菜单名称',
  `url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '菜单URL',
  `perms` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '授权(多个用逗号分隔,如:user:list,user:create)',
  `type` int DEFAULT NULL COMMENT '类型   0:目录   1:菜单   2:按钮',
  `icon` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '菜单图标',
  `order_num` int DEFAULT NULL COMMENT '排序',
  PRIMARY KEY (`menu_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='菜单管理';


insert  into `sys_menu`(`menu_id`,`parent_id`,`name`,`url`,`perms`,`type`,`icon`,`order_num`) values 
(1,0,'系统管理',NULL,NULL,0,'fa fa-cog',0),
(2,1,'管理员管理','modules/sys/user.html',NULL,1,'fa fa-user',1),
(3,1,'角色管理','modules/sys/role.html',NULL,1,'fa fa-user-secret',2),
(4,1,'菜单管理','modules/sys/menu.html',NULL,1,'fa fa-th-list',3),
(5,1,'SQL监控','druid/sql.html',NULL,1,'fa fa-bug',4),
(6,1,'定时任务','modules/job/schedule.html',NULL,1,'fa fa-tasks',5),
(7,6,'查看',NULL,'sys:schedule:list,sys:schedule:info',2,NULL,0),
(8,6,'新增',NULL,'sys:schedule:save',2,NULL,0),
(9,6,'修改',NULL,'sys:schedule:update',2,NULL,0),
(10,6,'删除',NULL,'sys:schedule:delete',2,NULL,0),
(11,6,'暂停',NULL,'sys:schedule:pause',2,NULL,0),
(12,6,'恢复',NULL,'sys:schedule:resume',2,NULL,0),
(13,6,'立即执行',NULL,'sys:schedule:run',2,NULL,0),
(14,6,'日志列表',NULL,'sys:schedule:log',2,NULL,0),
(15,2,'查看',NULL,'sys:user:list,sys:user:info',2,NULL,0),
(16,2,'新增',NULL,'sys:user:save,sys:role:select',2,NULL,0),
(17,2,'修改',NULL,'sys:user:update,sys:role:select',2,NULL,0),
(18,2,'删除',NULL,'sys:user:delete',2,NULL,0),
(19,3,'查看',NULL,'sys:role:list,sys:role:info',2,NULL,0),
(20,3,'新增',NULL,'sys:role:save,sys:menu:perms',2,NULL,0),
(21,3,'修改',NULL,'sys:role:update,sys:menu:perms',2,NULL,0),
(22,3,'删除',NULL,'sys:role:delete',2,NULL,0),
(23,4,'查看',NULL,'sys:menu:list,sys:menu:info',2,NULL,0),
(24,4,'新增',NULL,'sys:menu:save,sys:menu:select',2,NULL,0),
(25,4,'修改',NULL,'sys:menu:update,sys:menu:select',2,NULL,0),
(26,4,'删除',NULL,'sys:menu:delete',2,NULL,0),
(27,1,'参数管理','modules/sys/config.html','sys:config:list,sys:config:info,sys:config:save,sys:config:update,sys:config:delete',1,'fa fa-sun-o',6),
(29,1,'系统日志','modules/sys/log.html','sys:log:list',1,'fa fa-file-text-o',7),
(30,1,'文件上传','modules/oss/oss.html','sys:oss:all',1,'fa fa-file-image-o',6),
(31,1,'部门管理','modules/sys/dept.html',NULL,1,'fa fa-file-code-o',1),
(32,31,'查看',NULL,'sys:dept:list,sys:dept:info',2,NULL,0),
(33,31,'新增',NULL,'sys:dept:save,sys:dept:select',2,NULL,0),
(34,31,'修改',NULL,'sys:dept:update,sys:dept:select',2,NULL,0),
(35,31,'删除',NULL,'sys:dept:delete',2,NULL,0),
(36,1,'字典管理','modules/sys/dict.html',NULL,1,'fa fa-bookmark-o',6),
(37,36,'查看',NULL,'sys:dict:list,sys:dict:info',2,NULL,6),
(38,36,'新增',NULL,'sys:dict:save',2,NULL,6),
(39,36,'修改',NULL,'sys:dict:update',2,NULL,6),
(40,36,'删除',NULL,'sys:dict:delete',2,NULL,6);

DROP TABLE IF EXISTS `sys_role`;

CREATE TABLE `sys_role` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `role_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '角色名称',
  `user_id` bigint DEFAULT NULL COMMENT '用户id',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='角色';


insert  into `sys_role`(`id`,`role_name`,`user_id`,`create_time`) values 
(1,'admin',1,'2022-09-22 23:10:06');


DROP TABLE IF EXISTS `sys_role_menu`;

CREATE TABLE `sys_role_menu` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `role_id` bigint DEFAULT NULL COMMENT '角色ID',
  `menu_id` bigint DEFAULT '0' COMMENT '菜单id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;


insert  into `sys_role_menu`(`id`,`role_id`,`menu_id`) values 
(1,1,7),
(2,1,8);

三层

② IMarketUserService 权限 Service

public interface IMarketMenuService extends IService<MarketMenu> {

    /**
     * 按用户 ID 查找菜单
     * @param userId 用户id
     * @return
     */
    List<String> findMenuByUserId(Long userId);
}

③ MarketUserServiceImpl 权限 ServiceImpl

@Service
public class MarketMenuServiceImpl extends ServiceImpl<MarketMenuMapper,MarketMenu> implements IMarketMenuService {

    /**
     * 按用户 ID 查找菜单
     * @param userId 用户id
     * @return
     */
    @Override
    public List<String> findMenuByUserId(Long userId) {
        return this.baseMapper.findMenuByUserId(userId);
    }
}

③ MarketMenuMapper权限 Mapper

public interface MarketMenuMapper extends BaseMapper<MarketMenu> {

    /**
     * 按用户 ID 查找菜单
     * @param userId 用户id
     * @return
     */
    List<String> findMenuByUserId(@Param("userId") Long userId);
}

④ MarketMenuMapperXML权限 MapperXML

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.securitydemo.mapper.MarketMenuMapper">

    <select id="findMenuByUserId" resultType="string">
          select menu.perms from market_user AS user
          LEFT JOIN sys_role AS role ON user.user_id = role.user_id
          LEFT JOIN sys_role_menu AS role_menu ON role_id = role_menu.role_id
          LEFT JOIN sys_menu AS menu ON role_menu.menu_id = menu.menu_id
          WHERE user.user_id = #{userId}
    </select>

</mapper>

完善Sercurity 配置

⑤ SecurityConfig 添加注解 @EnableGlobalMethodSecurity(prePostEnabled = true)

/**
 * @Author: Lanys
 * @Description: Security 配置类
 * @Date: Create in 23:35 2022/8/27
 */
@Configuration
/** 开启权限信息 */
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    /**
     * 创建 BCryptPasswordEncoder 注入容器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        System.out.println("加载加密配置");
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 跨域
                .csrf().disable()
                // 不通过Session 获取 SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口,允许访问
                .antMatchers("/login","/doc.html","/swagger/**","/v2/api-docs"
                        ,"/swagger-ui.html","/swagger-resources/**","/webjars/**","/logout").anonymous()
                // 除外都要认证
                .anyRequest().authenticated();
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

⑥ UserDetailsServiceImpl 认证过程中添加权限

/**
 * @Author: Lanys
 * @Description:
 * @Date: Create in 22:35 2022/8/26
 */
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private IMarketUserService marketUserService;

    @Resource
    private IMarketMenuService marketMenuService;

    /**
     * 校验账号密码
     * @param username 账号
     * @return 用户信息
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {
            // 查询用户信息
            LambdaQueryWrapper<MarketUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
            lambdaQueryWrapper.eq(MarketUser::getUsername,username);
            MarketUser marketUser = this.marketUserService.getOne(lambdaQueryWrapper);
            if (marketUser == null) {
                throw  new RuntimeException("用户或密码错误");
            }
            log.info("[Security查询用户信息]参数:-> {}",marketUser.toString());
            // 查询权限
            List<String> strings = this.marketMenuService.findMenuByUserId(marketUser.getUserId());
            return new LoginUser(marketUser,strings);
        }catch (Exception e) {
            log.error("Security校验账号密码异常:",e);
        }
        return null;
    }
}

⑦ LoginUser 重载Security用户信息类保存权限

/**
 * @Author: Lanys
 * @Description: 实现 UserDetails
 * @Date: Create in 22:46 2022/8/26
 */
@Setter
@Getter
@NoArgsConstructor
public class LoginUser implements UserDetails, Serializable {

    private MarketUser marketUser;

    /** 存储权限信息 */
    private List<String> permissions;

    /** 专门装权限的 List */
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;

    public LoginUser(MarketUser marketUser) {
        this.marketUser = marketUser;
    }

    public LoginUser(MarketUser marketUser, List<String> permissions) {
        this.marketUser = marketUser;
        this.permissions = permissions;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (!CollectionUtils.isEmpty(authorities)) {
            return authorities;
        }
        // permissions 中 String 类型的权限信息封装成 SimpleGrantedAuthority 对象
        authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        return authorities;
    }

    @Override
    public String getPassword() {
        return this.marketUser.getPassword();
    }

    @Override
    public String getUsername() {
        return this.marketUser.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;
    }
}

⑧ JwtAuthenticationTokenFilter token过滤器将验证token成功后,解析token获取权限,将权限保存在Security Authentication中

/**
 * @Author: Lanys
 * @Description: security token过滤器
 * @Date: Create in 23:02 2022/8/28
 */
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private RedisUtils redisUtils;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        try {
            String authorization = httpServletRequest.getHeader("Authorization");
            // 没有token,放行
            if (StrUtil.isBlank(authorization)) {
                filterChain.doFilter(httpServletRequest,httpServletResponse);
                return;
            };
            // 有token逻辑
            log.info("过滤器校验token:{}",authorization);
            Claims claims = TokenUtil.parsingToken(authorization);
            String subject = claims.getSubject();
            if (StrUtil.isNotBlank(subject)) {
                Object obj = redisUtils.get(StrUtil.format(RedisConstants.USER_CATALOG,subject));
                LoginUser loginUser = null;
                if (obj != null) {
                    String toJSONString = JSON.toJSONString(obj);
                    Object parse = JSON.parse(toJSONString);
                    loginUser = JSONObject.parseObject(parse.toString(), LoginUser.class);
                }
                if (loginUser == null) {
                    throw new RuntimeException("用户未登录");
                }
                // 保存权限
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                filterChain.doFilter(httpServletRequest,httpServletResponse);
            }

        }catch (Exception e) {
            log.error("过滤器校验token异常:",e);
        }

    }
}

测试

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

相关文章:

  • jsp药品销售管理系统myeclipse开发sql数据库BS模式java编程网页结构
  • 银行卡四要素检测 易语言代码
  • 欧洲能源危机,这些企业将出现爆单情况
  • 我是怎么劝退打算入行软件测试的同学的?
  • iso9001质量体系认证
  • 珠磨机与球磨机的区别
  • 开发小程序,帮助民宿提高复购降低成本
  • 机器人地面站-[QGroundControl源码解析]-[7]-[api]
  • 对lua进行模糊测试及问题记录
  • VMware安装centOS7
  • Linux安全基线(二)配置8小项
  • 操作系统-第二章-调度与调度算法
  • 千年传承!敦煌遗址在云宇宙重建,背后还隐藏了哪些故事
  • IB课程热门科目你喜欢哪几个?
  • Node.js | JavaScript也能写后端?
  • Angular 2 DI - IoC DI - 1
  • CSS选择器——伪元素选择器之处理父元素高度及外边距溢出
  • ES6之路之模块详解
  • flutter的key在widget list的作用以及必要性
  • JS基础篇--通过JS生成由字母与数字组合的随机字符串
  • leetcode388. Longest Absolute File Path
  • node-sass 安装卡在 node scripts/install.js 解决办法
  • PHP的Ev教程三(Periodic watcher)
  • Python_OOP
  • sublime配置文件
  • Vue.js源码(2):初探List Rendering
  • Webpack入门之遇到的那些坑,系列示例Demo
  • WebSocket使用
  • 关于Flux,Vuex,Redux的思考
  • 理清楚Vue的结构
  • 前端自动化解决方案
  • 用简单代码看卷积组块发展
  • - 语言经验 - 《c++的高性能内存管理库tcmalloc和jemalloc》
  • Mac 上flink的安装与启动
  • ​人工智能书单(数学基础篇)
  • #Spring-boot高级
  • #我与Java虚拟机的故事#连载08:书读百遍其义自见
  • (ZT)北大教授朱青生给学生的一封信:大学,更是一个科学的保证
  • (超简单)使用vuepress搭建自己的博客并部署到github pages上
  • (附源码)ssm经济信息门户网站 毕业设计 141634
  • (论文阅读30/100)Convolutional Pose Machines
  • (学习日记)2024.01.19
  • (一)使用Mybatis实现在student数据库中插入一个学生信息
  • (转)c++ std::pair 与 std::make
  • (转载)跟我一起学习VIM - The Life Changing Editor
  • .NET DevOps 接入指南 | 1. GitLab 安装
  • .NET/C# 将一个命令行参数字符串转换为命令行参数数组 args
  • .NET/C# 项目如何优雅地设置条件编译符号?
  • .NET面试题解析(11)-SQL语言基础及数据库基本原理
  • .one4-V-XXXXXXXX勒索病毒数据怎么处理|数据解密恢复
  • .set 数据导入matlab,设置变量导入选项 - MATLAB setvaropts - MathWorks 中国
  • @Bean, @Component, @Configuration简析
  • [ C++ ] STL_list 使用及其模拟实现
  • [2017][note]基于空间交叉相位调制的两个连续波在few layer铋Bi中的全光switch——
  • [BJDCTF2020]The mystery of ip1