Spring Security起步零配置(一)

添加依赖

springboot的版本是 2.5.4,spring-security的版本是 5.5.2

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.5.4'
}

apply plugin: 'java'
apply plugin: 'io.spring.dependency-management'

group 'com.rick.security'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    implementation "org.springframework.boot:spring-boot-starter-security"
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

test {
    useJUnitPlatform()
}

添加接口

IndexController.java

@RestController
public class IndexController {

    @GetMapping("index")
    public String index() {
        return "index";
    }
}

启动服务

启动服务后控制台生成密码,并打印过滤器信息

Using generated security password: bc180105-93f7-4bb4-9b35-63ac46f54cdd

2021-10-09 11:50:53.874  INFO 32471 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@1948ea69, org.springframework.security.web.context.SecurityContextPersistenceFilter@56303475, org.springframework.security.web.header.HeaderWriterFilter@706cb08, org.springframework.security.web.csrf.CsrfFilter@69c93ca4, org.springframework.security.web.authentication.logout.LogoutFilter@5f13be1, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@99a78d7, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@62e6a3ec, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@47e4d9d0, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@2d746ce4, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@1dcca8d3, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@4632cfc, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@49798e84, org.springframework.security.web.session.SessionManagementFilter@6b68cb27, org.springframework.security.web.access.ExceptionTranslationFilter@10876a6, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@e4d2696]

访问接口:

GET http://localhost:8080/index

默认所有的资源都是受保护的,会跳转到登录页面http://localhost:8080/login
用户密码默认是 user,密码是控制台生成的。

http://xhope.top/wp-content/uploads/2021/10/login.png

登录之后,自动跳转到index。

Github:https://github.com/jkxyx205/spring-security-learn/tree/zero-config

sharp-common支持HttpMessageConverter对枚举Enum自定义转换

枚举类

StatusEnum code是String类型

@AllArgsConstructor
@Getter
public enum StatusEnum {
    DEFAULT("DEFAULT");

    @JsonValue
    public String getCode() {
        return this.name();
    }

    private final String label;

    public static StatusEnum valueOfCode(String code) {
        return valueOf(code);
    }
}

SexEnum code是int类型

@AllArgsConstructor
@Getter
public enum SexEnum {
    DEFAULT(1, "DEFAULT");

    private static final Map<Integer, SexEnum> codeMap = new HashMap<>();

    static {
        for (SexEnum e : values()) {
            codeMap.put(e.code, e);
        }
    }

    private final int code;

    private final String label;

    @JsonValue
    public int getCode() {
        return this.code;
    }

    public static SexEnum valueOfCode(int code) {
        return codeMap.get(code);
    }
}

枚举中必须包含静态方法 valueOfCode 和 方法 getCode

添加对Enum的转换配置

MvcConfig.java

@Configuration
@AllArgsConstructor
public class MvcConfig implements WebMvcConfigurer {

    private final ObjectMapper objectMapper;

    @Override
    public void addFormatters(FormatterRegistry registry) {
        // 排在前面优先使用 如果没有找到code仍然会尝试NAME。所以SexEnum可以通过1或者DEFAULT去反序列化
        registry.addConverterFactory(new CodeToEnumConverterFactory());
    }

    @PostConstruct
    public void postConstruct() {
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addDeserializer(Enum.class, new EnumJsonDeserializer());
        objectMapper.registerModule(simpleModule);
    }

}
  • addFormatters方法处理 FormHttpMessageConverter 对Enum的反序列化。如果没有找到code仍然会尝试NAME。所以SexEnum可以通过 1 或者 DEFAULT 去反序列化
  • postConstruct方法处理 MappingJackson2HttpMessageConverter 对Enum的反序列化

User.java

@Data
public class User {

    private SexEnum sex;

    private StatusEnum status;

    private String name;

}

测试

添加API接口

@RestController
public class TestController {

    @GetMapping("/test")
    public User test(@RequestBody User user) {
        System.out.println(user);
        return user;
    }

    @GetMapping("/test1")
    public User test1(User user) {
        System.out.println(user);
        return user;
    }
}

客户端请求

application/json 发起请求,后端 @RequestBody 接收数据

curl -X GET \
  http://127.0.0.1:8080/test \
  -H 'Content-Type: application/json' \
  -d '{
    "status": "DEFAULT",
    "name": "Rick",
    "sex": 1
}'

通过参数的方式发起数据

curl -X GET \
  'http://127.0.0.1:8080/test1?sex=1&status=DEFAULT&name=Rick'

curl -X GET \
  'http://127.0.0.1:8080/test1?sex=DEFAULT&status=DEFAULT&name=Rick'

Validation in Spring Boot

添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-validation</artifactId> 
</dependency>

测试

PersonForm.java

@Data
public class PersonForm {

    @NotNull
    @Size(min=2, max=30)
    private String name;

    @NotNull
    @Min(18)
    private Integer age;
}

IndexController.java

@PostMapping("/check")
public String checkPersonInfo(@Valid PersonForm personForm, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        FieldError fieldError = (FieldError) bindingResult.getAllErrors().get(0);
        System.out.println(fieldError.getField() + "-" + fieldError.getRejectedValue() + "-" +fieldError.getCode() + "-" + fieldError.getDefaultMessage());
        // output: age-11-Min-最小不能小于18
        return "form";
    }

    return "redirect:/results";
}

通过参数 @Valid 表示需要对PersonForm实例进行验证。第二个参数BindingResult 查看是否验证有错误。这个参数必须紧跟@Valid的Bean后面。如果没有指定参数 BindingResult,那么将会抛出异常 MethodArgumentNotValidException

编程式验证

  • 在spring环境下直接注入
private final Validator validator;
  • 验证
@GetMapping("/form")
public String form(PersonForm personForm) throws BindException {
    validate(personForm);
    return "form";
}

private <T> void validate(T t) throws BindException {
    Map<String, Object> map = new HashMap<>(16);
    BindingResult errors =  new MapBindingResult(map, t.getClass().getName());
    validator.validate(t, errors);
    if (errors.hasErrors()) {
        throw new BindException(errors);
    }
}

自定义验证规则

  • 添加验证器 PhoneValidator 验证手机号码
public class PhoneValidator implements ConstraintValidator<PhoneValid, String> {

    private static final String MOBILE_REGEX = "^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(17[013678])|(18[0,5-9]))\\d{8}$";

    private static final int MOBILE_LENGTH = 11;

    @Override
    public boolean isValid(String phone, ConstraintValidatorContext context) {
        if (Objects.isNull(phone)) {
            return true;
        }

        if (phone.length() != MOBILE_LENGTH) {
            return false;
        } else {
            Pattern p = Pattern.compile(MOBILE_REGEX);
            Matcher m = p.matcher(phone);
            if (m.matches()) {
                return true;
            }
        }
        return false;
    }
}
  • 添加注解 PhoneValid
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(
    validatedBy = {PhoneValidator.class}
)
public @interface PhoneValid {
    String message() default "手机号码格式不正确";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
  • 测试

PersonForm 添加属性手机号:

@PhoneValid
private String phone;

验证:

@GetMapping("/form")
public String form(PersonForm personForm) throws BindException {
    personForm.setName("Rick.Xu");
    personForm.setAge(19);
    personForm.setPhone("1232");
    validate(personForm);
    return "form";
}

控制台打印

.validation.BindException: org.springframework.validation.MapBindingResult: 1 errors
Field error in object 'com.rick.security.api.PersonForm' on field 'phone': rejected value [1232]; codes [PhoneValid.com.rick.security.api.PersonForm.phone,PhoneValid.phone,PhoneValid]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [com.rick.security.api.PersonForm.phone,phone]; arguments []; default message [phone]]; default message [手机号码格式不正确]]

参考文章

sharp-common集成验证

集成

注册ValidatorHelper.java

@Configuration
@RequiredArgsConstructor
public class AppConfig {

    private final Validator validator;

    @Bean
    public ValidatorHelper validatorHelper() {
        return new ValidatorHelper(validator);
    }
}

测试

User.java

@Value
@Builder
public class User {

    @PhoneValid
    private String phone;

    @NotBlank
    private String name;
}

Test.java

@Autowired
private ValidatorHelper validatorHelper;

@Test
public void testValidate() {
    User user = User.builder().name("rick").phone("18898987765").build();
    validatorHelper.validate(user);
}

@Test(expected = IllegalArgumentException.class)
public void testValidate2() {
    User user = User.builder().name("Rick").phone("188").build();
    validatorHelper.validate(user);
}

自定义验证

枚举值验证

枚举SexEnum UserStatusEnum

@AllArgsConstructor
@Getter
public enum SexEnum {
    FEMALE(0, "FEMALE"),
    MALE(1, "MALE");

    private static final Map<Integer, SexEnum> codeMap = new HashMap<>();

    static {
        for (SexEnum e: values()) {
            codeMap.put(e.code, e);
        }
    }

    private final Integer code;

    private final String label;

    public static SexEnum valueOfCode(Integer code) {
        return codeMap.get(code);
    }
}

@AllArgsConstructor
@Getter
public enum UserStatusEnum {
    NORMAL("NORMAL"),
    LOCKED("LOCKED");

    private final String label;

    public String getCode() {
        return this.name();
    }

    public static UserStatusEnum valueOfCode(String code) {
        return valueOf(code);
    }
}

枚举中必须包含静态方法 valueOfCode 和 方法 getCode

User 中添加属性

@Value
@Builder
public class User {

    @PhoneValid
    private String phone;

    @NotBlank
    private String name;

//    @EnumValid(target = SexEnum.class, message = "性别有误,不存在值${validatedValue}")
    @EnumValid(target = SexEnum.class)
    private int sex;

    @EnumValid(target = UserStatusEnum.class, message = "用户状态有误,不存在值${validatedValue}")
    private String userStatus;
}

测试:

@Test
public void testValidate3() throws BindException {
    User user = User.builder().name("rick").phone("18898987765").sex(1).userStatus("LOCKED").build();
    validatorHelper.validate(user);
}

@Test(expected = BindException.class)
public void testValidate4() throws BindException {
    User user = User.builder().name("rick").phone("18898987765").sex(3).userStatus("ENABLED").build();
    validatorHelper.validate(user);
}

sharp-fileupload文档转换(五)

PDF 转图片

  • pdf按页转多张图片
@Test
public void testPdf2Image1() throws IOException {
    File file = new File("/Users/rick/jkxyx205/tmp/fileupload/pdf/1.pdf");
    FileMeta fileMeta = FileMetaUtils.parse(file);

    List<byte[]> list = FileConvertUtils.pdf2Image(fileMeta.getData(), 150);
    File folder = new File("/Users/rick/jkxyx205/tmp/fileupload/pdf");
    for (int i = 0; i < list.size(); i++) {
        FileUtils.writeByteArrayToFile(new File(folder, i + ".png"), list.get(i));
    }
}
  • pdf转一张图片
@Test
public void testPdf2Image2() throws IOException {
    File file = new File("/Users/rick/jkxyx205/tmp/fileupload/pdf/1.pdf");
    FileMeta fileMeta = FileMetaUtils.parse(file);

    FileConvertUtils.pdf2Image(fileMeta.getData(),
            new FileOutputStream("/Users/rick/jkxyx205/tmp/fileupload/pdf/full.png"),
            150);
}