swagger,swagger-beauty,security,rbac-shiro

spring-boot-demo-swagger

集成原生 swagger ,自动生成 API 文档。>
启动项目,访问地址:http://localhost:8080/demo/swagger-ui.html#/

pom.xml


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <artifactId>spring-boot-demo-swaggerartifactId>
    <version>1.0.0-SNAPSHOTversion>
    <packaging>jarpackaging>
    <parent>
        <groupId>com.xkcodinggroupId>
        <artifactId>spring-boot-demoartifactId>
        <version>1.0.0-SNAPSHOTversion>
    parent>
    <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
        <java.version>1.8java.version>
        <swagger.version>2.9.2swagger.version>
    properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>io.springfoxgroupId>
            <artifactId>springfox-swagger2artifactId>
            <version>${swagger.version}version>
        dependency>
        <dependency>
            <groupId>io.springfoxgroupId>
            <artifactId>springfox-swagger-uiartifactId>
            <version>${swagger.version}version>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
    dependencies>
    <build>
        <finalName>spring-boot-demo-swaggerfinalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>
project>

Swagger2Config.java

@Configuration
@EnableSwagger2
public class Swagger2Config {
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.xkcoding.swagger.controller"))
                .paths(PathSelectors.any())
                .build();
    }
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder().title("spring-boot-demo")
                .description("这是一个简单的 Swagger API 演示")
                .contact(new Contact("Yangkai.Shen", "http://xkcoding.com", "[email protected]"))
                .version("1.0.0-SNAPSHOT")
                .build();
    }
}

UserController.java

主要演示API层的注解。

@RestController
@RequestMapping("/user")
@Api(tags = "1.0.0-SNAPSHOT", description = "用户管理", value = "用户管理")
@Slf4j
public class UserController {
    @GetMapping
    @ApiOperation(value = "条件查询(DONE)", notes = "备注")
    @ApiImplicitParams({@ApiImplicitParam(name = "username", value = "用户名", dataType = DataType.STRING, paramType = ParamType.QUERY, defaultValue = "xxx")})
    public ApiResponse<User> getByUserName(String username) {
        log.info("多个参数用  @ApiImplicitParams");
        return ApiResponse.<User>builder().code(200)
                .message("操作成功")
                .data(new User(1, username, "JAVA"))
                .build();
    }

    @GetMapping("/{id}")
    @ApiOperation(value = "主键查询(DONE)", notes = "备注")
    @ApiImplicitParams({@ApiImplicitParam(name = "id", value = "用户编号", dataType = DataType.INT, paramType = ParamType.PATH)})
    public ApiResponse<User> get(@PathVariable Integer id) {
        log.info("单个参数用  @ApiImplicitParam");
        return ApiResponse.<User>builder().code(200)
                .message("操作成功")
                .data(new User(id, "u1", "p1"))
                .build();
    }
    @DeleteMapping("/{id}")
    @ApiOperation(value = "删除用户(DONE)", notes = "备注")
    @ApiImplicitParam(name = "id", value = "用户编号", dataType = DataType.INT, paramType = ParamType.PATH)
    public void delete(@PathVariable Integer id) {
        log.info("单个参数用 ApiImplicitParam");
    }
    @PostMapping
    @ApiOperation(value = "添加用户(DONE)")
    public User post(@RequestBody User user) {
        log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam");
        return user;
    }
    @PostMapping("/multipar")
    @ApiOperation(value = "添加用户(DONE)")
    public List<User> multipar(@RequestBody List<User> user) {
        log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam");
        return user;
    }
    @PostMapping("/array")
    @ApiOperation(value = "添加用户(DONE)")
    public User[] array(@RequestBody User[] user) {
        log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam");
        return user;
    }
    @PutMapping("/{id}")
    @ApiOperation(value = "修改用户(DONE)")
    public void put(@PathVariable Long id, @RequestBody User user) {
        log.info("如果你不想写 @ApiImplicitParam 那么 swagger 也会使用默认的参数名作为描述信息 ");
    }
    @PostMapping("/{id}/file")
    @ApiOperation(value = "文件上传(DONE)")
    public String file(@PathVariable Long id, @RequestParam("file") MultipartFile file) {
        log.info(file.getContentType());
        log.info(file.getName());
        log.info(file.getOriginalFilename());
        return file.getOriginalFilename();
    }
}

ApiResponse.java

主要演示了 实体类 的注解。

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "通用PI接口返回", description = "Common Api Response")
public class ApiResponse<T> implements Serializable {
    private static final long serialVersionUID = -8987146499044811408L;

    @ApiModelProperty(value = "通用返回状态", required = true)
    private Integer code;

    @ApiModelProperty(value = "通用返回信息", required = true)
    private String message;

    @ApiModelProperty(value = "通用返回数据", required = true)
    private T data;
}

参考

  1. swagger 官方网站:https://swagger.io/
  2. swagger 官方文档:https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X—Getting-started
  3. swagger 常用注解:https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X—Annotations

spring-boot-demo-swagger-beauty

(https://github.com/battcn/swagger-spring-boot) 集成。
启动项目,访问地址:http://localhost:8080/demo/swagger-ui.html#/
用户名:xkcoding
密码:123456

pom.xml


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <artifactId>spring-boot-demo-swagger-beautyartifactId>
    <version>1.0.0-SNAPSHOTversion>
    <packaging>jarpackaging>
    <parent>
        <groupId>com.xkcodinggroupId>
        <artifactId>spring-boot-demoartifactId>
        <version>1.0.0-SNAPSHOTversion>
    parent>
    <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
        <java.version>1.8java.version>
        <battcn.swagger.version>2.1.2-RELEASEbattcn.swagger.version>
    properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>com.battcngroupId>
            <artifactId>swagger-spring-boot-starterartifactId>
            <version>${battcn.swagger.version}version>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
    dependencies>
    <build>
        <finalName>spring-boot-demo-swagger-beautyfinalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>
project>

application.yml

server:
  port: 8080
  servlet:
    context-path: /demo
spring:
  swagger:
    enabled: true
    title: spring-boot-demo
    description: 这是一个简单的 Swagger API 演示
    version: 1.0.0-SNAPSHOT
    contact:
      name: Yangkai.Shen
      email: [email protected]
      url: http://xkcoding.com
    # swagger扫描的基础包,默认:全扫描
    # base-package:
    # 需要处理的基础URL规则,默认:/**
    # base-path:
    # 需要排除的URL规则,默认:空
    # exclude-path:
    security:
      # 是否启用 swagger 登录验证
      filter-plugin: true
      username: xkcoding
      password: 123456
    global-response-messages:
      GET[0]:
        code: 400
        message: Bad Request,一般为请求参数不对
      GET[1]:
        code: 404
        message: NOT FOUND,一般为请求路径不对
      GET[2]:
        code: 500
        message: ERROR,一般为程序内部错误
      POST[0]:
        code: 400
        message: Bad Request,一般为请求参数不对
      POST[1]:
        code: 404
        message: NOT FOUND,一般为请求路径不对
      POST[2]:
        code: 500
        message: ERROR,一般为程序内部错误

ApiResponse.java

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "通用PI接口返回", description = "Common Api Response")
public class ApiResponse<T> implements Serializable {
    private static final long serialVersionUID = -8987146499044811408L;
    @ApiModelProperty(value = "通用返回状态", required = true)
    private Integer code;
    @ApiModelProperty(value = "通用返回信息", required = true)
    private String message;
    @ApiModelProperty(value = "通用返回数据", required = true)
    private T data;
}

User.java

@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "用户实体", description = "User Entity")
public class User implements Serializable {
    private static final long serialVersionUID = 5057954049311281252L;
    @ApiModelProperty(value = "主键id", required = true)
    private Integer id;
    @ApiModelProperty(value = "用户名", required = true)
    private String name;
    @ApiModelProperty(value = "工作岗位", required = true)
    private String job;
}

UserController.java

@RestController
@RequestMapping("/user")
@Api(tags = "1.0.0-SNAPSHOT", description = "用户管理", value = "用户管理")
@Slf4j
public class UserController {
    @GetMapping
    @ApiOperation(value = "条件查询(DONE)", notes = "备注")
    @ApiImplicitParams({@ApiImplicitParam(name = "username", value = "用户名", dataType = DataType.STRING, paramType = ParamType.QUERY, defaultValue = "xxx")})
    public ApiResponse<User> getByUserName(String username) {
        log.info("多个参数用  @ApiImplicitParams");
        return ApiResponse.<User>builder().code(200).message("操作成功").data(new User(1, username, "JAVA")).build();
    }
    @GetMapping("/{id}")
    @ApiOperation(value = "主键查询(DONE)", notes = "备注")
    @ApiImplicitParams({@ApiImplicitParam(name = "id", value = "用户编号", dataType = DataType.INT, paramType = ParamType.PATH)})
    public ApiResponse<User> get(@PathVariable Integer id) {
        log.info("单个参数用  @ApiImplicitParam");
        return ApiResponse.<User>builder().code(200).message("操作成功").data(new User(id, "u1", "p1")).build();
    }

    @DeleteMapping("/{id}")
    @ApiOperation(value = "删除用户(DONE)", notes = "备注")
    @ApiImplicitParam(name = "id", value = "用户编号", dataType = DataType.INT, paramType = ParamType.PATH)
    public void delete(@PathVariable Integer id) {
        log.info("单个参数用 ApiImplicitParam");
    }

    @PostMapping
    @ApiOperation(value = "添加用户(DONE)")
    public User post(@RequestBody User user) {
        log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam");
        return user;
    }

    @PostMapping("/multipar")
    @ApiOperation(value = "添加用户(DONE)")
    public List<User> multipar(@RequestBody List<User> user) {
        log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam");

        return user;
    }

    @PostMapping("/array")
    @ApiOperation(value = "添加用户(DONE)")
    public User[] array(@RequestBody User[] user) {
        log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam");
        return user;
    }

    @PutMapping("/{id}")
    @ApiOperation(value = "修改用户(DONE)")
    public void put(@PathVariable Long id, @RequestBody User user) {
        log.info("如果你不想写 @ApiImplicitParam 那么 swagger 也会使用默认的参数名作为描述信息 ");
    }

    @PostMapping("/{id}/file")
    @ApiOperation(value = "文件上传(DONE)")
    public String file(@PathVariable Long id, @RequestParam("file") MultipartFile file) {
        log.info(file.getContentType());
        log.info(file.getName());
        log.info(file.getOriginalFilename());
        return file.getOriginalFilename();
    }
}

参考

  • https://github.com/battcn/swagger-spring-boot/blob/master/README.md
  • 几款比较好看的swagger-ui,具体使用方法参见各个依赖的官方文档:
    • battcn 的 swagger-spring-boot-starter 文档:https://github.com/battcn/swagger-spring-boot/blob/master/README.md
    • swagger-ui-layer 文档:https://gitee.com/caspar-chen/Swagger-UI-layer#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8
    • swagger-bootstrap-ui 文档:https://gitee.com/xiaoym/swagger-bootstrap-ui#%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E
    • swagger-ui-themes 文档:https://github.com/ostranme/swagger-ui-themes#getting-started

spring-boot-demo-rbac-security

pom.xml


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <artifactId>spring-boot-demo-rbac-securityartifactId>
    <version>1.0.0-SNAPSHOTversion>
    <packaging>jarpackaging>
    <parent>
        <groupId>com.xkcodinggroupId>
        <artifactId>spring-boot-demoartifactId>
        <version>1.0.0-SNAPSHOTversion>
    parent>
    <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
        <java.version>1.8java.version>
        <jjwt.veersion>0.9.1jjwt.veersion>
    properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-jpaartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        
        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-pool2artifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-configuration-processorartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>io.jsonwebtokengroupId>
            <artifactId>jjwtartifactId>
            <version>${jjwt.veersion}version>
        dependency>
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>cn.hutoolgroupId>
            <artifactId>hutool-allartifactId>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
    dependencies>
    <build>
        <finalName>spring-boot-demo-rbac-securityfinalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>
project>

3.2. JwtUtil.java

JWT 工具类,主要功能:生成JWT并存入Redis、解析JWT并校验其准确性、从Request的Header中获取JWT

@EnableConfigurationProperties(JwtConfig.class)
@Configuration
@Slf4j
public class JwtUtil {
    @Autowired
    private JwtConfig jwtConfig;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 创建JWT
     *
     * @param rememberMe  记住我
     * @param id          用户id
     * @param subject     用户名
     * @param roles       用户角色
     * @param authorities 用户权限
     * @return JWT
     */
    public String createJWT(Boolean rememberMe, Long id, String subject, List<String> roles, Collection<? extends GrantedAuthority> authorities) {
        Date now = new Date();
        JwtBuilder builder = Jwts.builder()
                .setId(id.toString())
                .setSubject(subject)
                .setIssuedAt(now)
                .signWith(SignatureAlgorithm.HS256, jwtConfig.getKey())
                .claim("roles", roles)
                .claim("authorities", authorities);

        // 设置过期时间
        Long ttl = rememberMe ? jwtConfig.getRemember() : jwtConfig.getTtl();
        if (ttl > 0) {
            builder.setExpiration(DateUtil.offsetMillisecond(now, ttl.intValue()));
        }

        String jwt = builder.compact();
        // 将生成的JWT保存至Redis
        stringRedisTemplate.opsForValue()
                .set(Consts.REDIS_JWT_KEY_PREFIX + subject, jwt, ttl, TimeUnit.MILLISECONDS);
        return jwt;
    }

    /**
     * 创建JWT
     *
     * @param authentication 用户认证信息
     * @param rememberMe     记住我
     * @return JWT
     */
    public String createJWT(Authentication authentication, Boolean rememberMe) {
        UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
        return createJWT(rememberMe, userPrincipal.getId(), userPrincipal.getUsername(), userPrincipal.getRoles(), userPrincipal.getAuthorities());
    }

    /**
     * 解析JWT
     *
     * @param jwt JWT
     * @return {@link Claims}
     */
    public Claims parseJWT(String jwt) {
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(jwtConfig.getKey())
                    .parseClaimsJws(jwt)
                    .getBody();

            String username = claims.getSubject();
            String redisKey = Consts.REDIS_JWT_KEY_PREFIX + username;

            // 校验redis中的JWT是否存在
            Long expire = stringRedisTemplate.getExpire(redisKey, TimeUnit.MILLISECONDS);
            if (Objects.isNull(expire) || expire <= 0) {
                throw new SecurityException(Status.TOKEN_EXPIRED);
            }

            // 校验redis中的JWT是否与当前的一致,不一致则代表用户已注销/用户在不同设备登录,均代表JWT已过期
            String redisToken = stringRedisTemplate.opsForValue()
                    .get(redisKey);
            if (!StrUtil.equals(jwt, redisToken)) {
                throw new SecurityException(Status.TOKEN_OUT_OF_CTRL);
            }
            return claims;
        } catch (ExpiredJwtException e) {
            log.error("Token 已过期");
            throw new SecurityException(Status.TOKEN_EXPIRED);
        } catch (UnsupportedJwtException e) {
            log.error("不支持的 Token");
            throw new SecurityException(Status.TOKEN_PARSE_ERROR);
        } catch (MalformedJwtException e) {
            log.error("Token 无效");
            throw new SecurityException(Status.TOKEN_PARSE_ERROR);
        } catch (SignatureException e) {
            log.error("无效的 Token 签名");
            throw new SecurityException(Status.TOKEN_PARSE_ERROR);
        } catch (IllegalArgumentException e) {
            log.error("Token 参数不存在");
            throw new SecurityException(Status.TOKEN_PARSE_ERROR);
        }
    }

    /**
     * 设置JWT过期
     *
     * @param request 请求
     */
    public void invalidateJWT(HttpServletRequest request) {
        String jwt = getJwtFromRequest(request);
        String username = getUsernameFromJWT(jwt);
        // 从redis中清除JWT
        stringRedisTemplate.delete(Consts.REDIS_JWT_KEY_PREFIX + username);
    }

    /**
     * 根据 jwt 获取用户名
     *
     * @param jwt JWT
     * @return 用户名
     */
    public String getUsernameFromJWT(String jwt) {
        Claims claims = parseJWT(jwt);
        return claims.getSubject();
    }

    /**
     * 从 request 的 header 中获取 JWT
     *
     * @param request 请求
     * @return JWT
     */
    public String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }

}

3.3. SecurityConfig.java

Spring Security 配置类,主要功能:配置哪些URL不需要认证,哪些需要认证

@Configuration
@EnableWebSecurity
@EnableConfigurationProperties(CustomConfig.class)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private CustomConfig customConfig;

    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public BCryptPasswordEncoder encoder() {
        return new BCryptPasswordEncoder();
    }

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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserDetailsService)
                .passwordEncoder(encoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors()
                // 关闭 CSRF
                .and()
                .csrf()
                .disable()
                // 登录行为由自己实现,参考 AuthController#login
                .formLogin()
                .disable()
                .httpBasic()
                .disable()
                // 认证请求
                .authorizeRequests()
                // 所有请求都需要登录访问
                .anyRequest()
                .authenticated()
                // RBAC 动态 url 认证
                .anyRequest()
                .access("@rbacAuthorityService.hasPermission(request,authentication)")
                // 登出行为由自己实现,参考 AuthController#logout
                .and()
                .logout()
                .disable()
                // Session 管理
                .sessionManagement()
                // 因为使用了JWT,所以这里不管理Session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                // 异常处理
                .and()
                .exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler);

        // 添加自定义 JWT 过滤器
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }

    /**
     * 放行所有不需要登录就可以访问的请求,参见 AuthController
     * 也可以在 {@link #configure(HttpSecurity)} 中配置
     * {@code http.authorizeRequests().antMatchers("/api/auth/**").permitAll()}
     */
    @Override
    public void configure(WebSecurity web) {
        WebSecurity and = web.ignoring()
                .and();
        // 忽略 GET
        customConfig.getIgnores()
                .getGet()
                .forEach(url -> and.ignoring()
                        .antMatchers(HttpMethod.GET, url));
        // 忽略 POST
        customConfig.getIgnores()
                .getPost()
                .forEach(url -> and.ignoring()
                        .antMatchers(HttpMethod.POST, url));
        // 忽略 DELETE
        customConfig.getIgnores()
                .getDelete()
                .forEach(url -> and.ignoring()
                        .antMatchers(HttpMethod.DELETE, url));
        // 忽略 PUT
        customConfig.getIgnores()
                .getPut()
                .forEach(url -> and.ignoring()
                        .antMatchers(HttpMethod.PUT, url));
        // 忽略 HEAD
        customConfig.getIgnores()
                .getHead()
                .forEach(url -> and.ignoring()
                        .antMatchers(HttpMethod.HEAD, url));
        // 忽略 PATCH
        customConfig.getIgnores()
                .getPatch()
                .forEach(url -> and.ignoring()
                        .antMatchers(HttpMethod.PATCH, url));
        // 忽略 OPTIONS
        customConfig.getIgnores()
                .getOptions()
                .forEach(url -> and.ignoring()
                        .antMatchers(HttpMethod.OPTIONS, url));
        // 忽略 TRACE
        customConfig.getIgnores()
                .getTrace()
                .forEach(url -> and.ignoring()
                        .antMatchers(HttpMethod.TRACE, url));
        // 按照请求格式忽略
        customConfig.getIgnores()
                .getPattern()
                .forEach(url -> and.ignoring()
                        .antMatchers(url));

    }
}

3.4. RbacAuthorityService.java

/**
 * 

* 动态路由认证 *

* * @author yangkai.shen * @date Created in 2018-12-10 17:17 */
@Component public class RbacAuthorityService { @Autowired private RoleDao roleDao; @Autowired private PermissionDao permissionDao; @Autowired private RequestMappingHandlerMapping mapping; public boolean hasPermission(HttpServletRequest request, Authentication authentication) { checkRequest(request); Object userInfo = authentication.getPrincipal(); boolean hasPermission = false; if (userInfo instanceof UserDetails) { UserPrincipal principal = (UserPrincipal) userInfo; Long userId = principal.getId(); List<Role> roles = roleDao.selectByUserId(userId); List<Long> roleIds = roles.stream() .map(Role::getId) .collect(Collectors.toList()); List<Permission> permissions = permissionDao.selectByRoleIdList(roleIds); //获取资源,前后端分离,所以过滤页面权限,只保留按钮权限 List<Permission> btnPerms = permissions.stream() // 过滤页面权限 .filter(permission -> Objects.equals(permission.getType(), Consts.BUTTON)) // 过滤 URL 为空 .filter(permission -> StrUtil.isNotBlank(permission.getUrl())) // 过滤 METHOD 为空 .filter(permission -> StrUtil.isNotBlank(permission.getMethod())) .collect(Collectors.toList()); for (Permission btnPerm : btnPerms) { AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(btnPerm.getUrl(), btnPerm.getMethod()); if (antPathMatcher.matches(request)) { hasPermission = true; break; } } return hasPermission; } else { return false; } } /** * 校验请求是否存在 * * @param request 请求 */ private void checkRequest(HttpServletRequest request) { // 获取当前 request 的方法 String currentMethod = request.getMethod(); Multimap<String, String> urlMapping = allUrlMapping(); for (String uri : urlMapping.keySet()) { // 通过 AntPathRequestMatcher 匹配 url // 可以通过 2 种方式创建 AntPathRequestMatcher // 1:new AntPathRequestMatcher(uri,method) 这种方式可以直接判断方法是否匹配,因为这里我们把 方法不匹配 自定义抛出,所以,我们使用第2种方式创建 // 2:new AntPathRequestMatcher(uri) 这种方式不校验请求方法,只校验请求路径 AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(uri); if (antPathMatcher.matches(request)) { if (!urlMapping.get(uri) .contains(currentMethod)) { throw new SecurityException(Status.HTTP_BAD_METHOD); } else { return; } } } throw new SecurityException(Status.REQUEST_NOT_FOUND); } /** * 获取 所有URL Mapping,返回格式为{"/test":["GET","POST"],"/sys":["GET","DELETE"]} * * @return {@link ArrayListMultimap} 格式的 URL Mapping */ private Multimap<String, String> allUrlMapping() { Multimap<String, String> urlMapping = ArrayListMultimap.create(); // 获取url与类和方法的对应信息 Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods(); handlerMethods.forEach((k, v) -> { // 获取当前 key 下的获取所有URL Set<String> url = k.getPatternsCondition() .getPatterns(); RequestMethodsRequestCondition method = k.getMethodsCondition(); // 为每个URL添加所有的请求方法 url.forEach(s -> urlMapping.putAll(s, method.getMethods() .stream() .map(Enum::toString) .collect(Collectors.toList()))); }); return urlMapping; } }

3.5. JwtAuthenticationFilter.java

/**
 * 

* Jwt 认证过滤器 *

* * @author yangkai.shen * @date Created in 2018-12-10 15:15 */
@Component @Slf4j public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private CustomUserDetailsService customUserDetailsService; @Autowired private JwtUtil jwtUtil; @Autowired private CustomConfig customConfig; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (checkIgnores(request)) { filterChain.doFilter(request, response); return; } String jwt = jwtUtil.getJwtFromRequest(request); if (StrUtil.isNotBlank(jwt)) { try { String username = jwtUtil.getUsernameFromJWT(jwt); UserDetails userDetails = customUserDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext() .setAuthentication(authentication); filterChain.doFilter(request, response); } catch (SecurityException e) { ResponseUtil.renderJson(response, e); } } else { ResponseUtil.renderJson(response, Status.UNAUTHORIZED, null); } } /** * 请求是否不需要进行权限拦截 * * @param request 当前请求 * @return true - 忽略,false - 不忽略 */ private boolean checkIgnores(HttpServletRequest request) { String method = request.getMethod(); HttpMethod httpMethod = HttpMethod.resolve(method); if (ObjectUtil.isNull(httpMethod)) { httpMethod = HttpMethod.GET; } Set<String> ignores = Sets.newHashSet(); switch (httpMethod) { case GET: ignores.addAll(customConfig.getIgnores() .getGet()); break; case PUT: ignores.addAll(customConfig.getIgnores() .getPut()); break; case HEAD: ignores.addAll(customConfig.getIgnores() .getHead()); break; case POST: ignores.addAll(customConfig.getIgnores() .getPost()); break; case PATCH: ignores.addAll(customConfig.getIgnores() .getPatch()); break; case TRACE: ignores.addAll(customConfig.getIgnores() .getTrace()); break; case DELETE: ignores.addAll(customConfig.getIgnores() .getDelete()); break; case OPTIONS: ignores.addAll(customConfig.getIgnores() .getOptions()); break; default: break; } ignores.addAll(customConfig.getIgnores() .getPattern()); if (CollUtil.isNotEmpty(ignores)) { for (String ignore : ignores) { AntPathRequestMatcher matcher = new AntPathRequestMatcher(ignore, method); if (matcher.matches(request)) { return true; } } } return false; } }

3.6. CustomUserDetailsService.java

实现 UserDetailsService 接口,主要功能:根据用户名查询用户信息

/**
 * 

* 自定义UserDetails查询 *

* * @author yangkai.shen * @date Created in 2018-12-10 10:29 */
@Service public class CustomUserDetailsService implements UserDetailsService { @Autowired private UserDao userDao; @Autowired private RoleDao roleDao; @Autowired private PermissionDao permissionDao; @Override public UserDetails loadUserByUsername(String usernameOrEmailOrPhone) throws UsernameNotFoundException { User user = userDao.findByUsernameOrEmailOrPhone(usernameOrEmailOrPhone, usernameOrEmailOrPhone, usernameOrEmailOrPhone) .orElseThrow(() -> new UsernameNotFoundException("未找到用户信息 : " + usernameOrEmailOrPhone)); List<Role> roles = roleDao.selectByUserId(user.getId()); List<Long> roleIds = roles.stream() .map(Role::getId) .collect(Collectors.toList()); List<Permission> permissions = permissionDao.selectByRoleIdList(roleIds); return UserPrincipal.create(user, roles, permissions); } }

3.7. RedisUtil.java

主要功能:根据key的格式分页获取Redis存在的key列表

@Component
@Slf4j
public class RedisUtil {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 分页获取指定格式key,使用 scan 命令代替 keys 命令,在大数据量的情况下可以提高查询效率
     *
     * @param patternKey  key格式
     * @param currentPage 当前页码
     * @param pageSize    每页条数
     * @return 分页获取指定格式key
     */
    public PageResult<String> findKeysForPage(String patternKey, int currentPage, int pageSize) {
        ScanOptions options = ScanOptions.scanOptions()
                .match(patternKey)
                .build();
        RedisConnectionFactory factory = stringRedisTemplate.getConnectionFactory();
        RedisConnection rc = factory.getConnection();
        Cursor<byte[]> cursor = rc.scan(options);
        List<String> result = Lists.newArrayList();
        long tmpIndex = 0;
        int startIndex = (currentPage - 1) * pageSize;
        int end = currentPage * pageSize;
        while (cursor.hasNext()) {
            String key = new String(cursor.next());
            if (tmpIndex >= startIndex && tmpIndex < end) {
                result.add(key);
            }
            tmpIndex++;
        }
        try {
            cursor.close();
            RedisConnectionUtils.releaseConnection(rc, factory);
        } catch (Exception e) {
            log.warn("Redis连接关闭异常,", e);
        }
        return new PageResult<>(result, tmpIndex);
    }
}

3.8. MonitorService.java

监控服务,主要功能:查询当前在线人数分页列表,手动踢出某个用户

package com.xkcoding.rbac.security.service;
import cn.hutool.core.util.StrUtil;
import com.google.common.collect.Lists;
import com.xkcoding.rbac.security.common.Consts;
import com.xkcoding.rbac.security.common.PageResult;
import com.xkcoding.rbac.security.model.User;
import com.xkcoding.rbac.security.repository.UserDao;
import com.xkcoding.rbac.security.util.RedisUtil;
import com.xkcoding.rbac.security.vo.OnlineUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

/**
 * 

* 监控 Service *

* * @author yangkai.shen * @date Created in 2018-12-12 00:55 */
@Service public class MonitorService { @Autowired private RedisUtil redisUtil; @Autowired private UserDao userDao; public PageResult<OnlineUser> onlineUser(Integer page, Integer size) { PageResult<String> keys = redisUtil.findKeysForPage(Consts.REDIS_JWT_KEY_PREFIX + Consts.SYMBOL_STAR, page, size); List<String> rows = keys.getRows(); Long total = keys.getTotal(); // 根据 redis 中键获取用户名列表 List<String> usernameList = rows.stream() .map(s -> StrUtil.subAfter(s, Consts.REDIS_JWT_KEY_PREFIX, true)) .collect(Collectors.toList()); // 根据用户名查询用户信息 List<User> userList = userDao.findByUsernameIn(usernameList); // 封装在线用户信息 List<OnlineUser> onlineUserList = Lists.newArrayList(); userList.forEach(user -> onlineUserList.add(OnlineUser.create(user))); return new PageResult<>(onlineUserList, total); } }

4. 参考

  1. Spring Security 官方文档:https://docs.spring.io/spring-security/site/docs/5.1.1.RELEASE/reference/htmlsingle/
  2. JWT 官网:https://jwt.io/
  3. JJWT开源工具参考:https://github.com/jwtk/jjwt#quickstart
  4. 授权部分参考官方文档:https://docs.spring.io/spring-security/site/docs/5.1.1.RELEASE/reference/htmlsingle/#authorization
  5. 动态授权部分,参考博客:https://blog.csdn.net/larger5/article/details/81063438

rbac-shiro

pom.xml


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <artifactId>demo-rbac-shiroartifactId>
    <version>1.0.0-SNAPSHOTversion>
    <packaging>jarpackaging>
    <parent>
        <groupId>com.xkcodinggroupId>
        <artifactId>spring-boot-demoartifactId>
        <version>1.0.0-SNAPSHOTversion>
    parent>
    <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
        <java.version>1.8java.version>
    properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-starter-tomcatartifactId>
                exclusion>
            exclusions>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-undertowartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>3.1.0version>
        dependency>
        
        <dependency>
            <groupId>p6spygroupId>
            <artifactId>p6spyartifactId>
            <version>3.8.1version>
        dependency>
        
        <dependency>
            <groupId>org.apache.shirogroupId>
            <artifactId>shiro-spring-boot-starterartifactId>
            <version>1.4.0version>
        dependency>
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>
        <dependency>
            <groupId>cn.hutoolgroupId>
            <artifactId>hutool-allartifactId>
        dependency>
        <dependency>
            <groupId>com.google.guavagroupId>
            <artifactId>guavaartifactId>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
    dependencies>
    <build>
        <finalName>demo-rbac-shirofinalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>
project>

yml

server:
  port: 8080
  servlet:
    context-path: /demo
spring:
  datasource:
    hikari:
      username: root
      password: root
    driver-class-name: com.p6spy.engine.spy.P6SpyDriver
    url: jdbc:p6spy:mysql://127.0.0.1:3306/spring-boot-demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
mybatis-plus:
  global-config:
    # 关闭banner
    banner: false

spy.properties

module.log=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 设置 p6spy driver 代理
deregisterdrivers=true
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,batch,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 实际驱动可多个
#driverlist=org.h2.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2

MybatisPlusConfig.java

@Configuration
public class MybatisPlusConfig {
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        List<ISqlParser> sqlParserList = new ArrayList<>();
        // 攻击 SQL 阻断解析器、加入解析链
        sqlParserList.add(new BlockAttackSqlParser());
        paginationInterceptor.setSqlParserList(sqlParserList);
        return paginationInterceptor;
    }
    /**
     * SQL执行效率插件
     */
    @Bean
    public PerformanceInterceptor performanceInterceptor() {
        return new PerformanceInterceptor();
    }
}

你可能感兴趣的:(java,spring,servlet)