【Spring Security从入门到进阶】(二)密码的进化和验证 | Eddie'Blog
【Spring Security从入门到进阶】(二)密码的进化和验证

【Spring Security从入门到进阶】(二)密码的进化和验证

eddie 445 2021-09-23

@[toc]

目录

Spring Security 之后会学习章节迁移到
CSDN - Spring Security 专栏

(二)密码验证

2.2.1 密码编辑器

  • MessageDigestPasswordEncoder
    • 已经过时了,但仍然保留原因是防止老旧项目还使用MD5加密的
  • Pbkdf2PasswordEncoder
    • 该加密不存在特殊字符串
  • BCryptPasswordEncoder
    • 该加密存在特殊字符串

 @Override
 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
     auth.inMemoryAuthentication()
             .withUser("eddie")
             .password(passwordEncoder().encode("123456"))
             .roles("USER","ADMIN")
     ;
 }

 @Bean
 PasswordEncoder passwordEncoder() {
      return new MessageDigestPasswordEncoder("MD5");
//	    return new Pbkdf2PasswordEncoder();
//	    return new BCryptPasswordEncoder();
}

在这里插入图片描述

2.2.2 兼容不同的加密方式的办法(非必要不推荐)

DelegatingPasswordEncoder 允许以不同的格式验证密码,提供升级的可能性

在这里插入图片描述如图所示:复制 Value = $2a$10$RYV46Kzw56EH0d.qek.7.OOBAdWJql8jdyJypeV0/du5et1L6ShVm

在这里插入图片描述如图所示:复制 Value = {2mn61IJDaNrHsZ/aX7sk5HU+jedNaoFseoejzmnzu/Y=}5b9505f2babef46787597f8ef6c3af3852379e5e

2.2.2.1 Security 配置

  • 解除屏蔽的表单部分代码
  • 指定 eddie 和 lee 账户使用不同的加密方式
    • 解决出现旧用户是某些加密算法,而重构后另外一种加密算法
@Slf4j
// (debug = true) 请勿在生产系统中使用
@EnableWebSecurity(debug = true)
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final ObjectMapper objectMapper;

	// 在父类的352行
	@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 所有路径都需要认证
                .authorizeRequests(r -> r
                        .antMatchers("/authorize/**").permitAll()
                        .antMatchers("/admin/**").hasRole("ADMIN")
                        .antMatchers("/api/**").hasRole("USER")
                        .anyRequest().authenticated())
                // restAuthenticationFilter() 替代 UsernamePasswordAuthenticationFilter.class 过滤器
                .addFilterAt(restAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                // 这几个路径不使用csrf token
                .csrf(c -> c.ignoringAntMatchers("/authorize/**","/admin/**","/api/**"))
                // 不添加 permitAll() 的话,就会一直“重定向”
//=============================================================
                .formLogin(f -> f.loginPage("/login")
//                        .usernameParameter("username1")  // 指定username参数为username1,但需要前端配合传参也是 username1
                        // 登录成功,返回json
                        .defaultSuccessUrl("/")
                        .successHandler(jsonAuthenticationSuccessHandler())
                        // 登录失败,返回json
                        .failureHandler(jsonAuthenticationFailureHandler())
                        .permitAll())
//                 显示浏览器对话框,需要禁用 CSRF ,或添加路径到忽略列表
                .httpBasic(Customizer.withDefaults())
                .csrf(Customizer.withDefaults())
//                 index.html  perform_logout 45行;  "/" 不能少,不然跳转404
                .logout(l -> l.logoutUrl("/perform_logout")
                        // 登出成功,返回json
//                        .logoutSuccessUrl("/login")
                        .logoutSuccessHandler(jsonLogoutSuccessHandler())
                        .deleteCookies("JSESSIONID")
                )
                .rememberMe(r -> r.tokenValiditySeconds(30 * 24 * 3600)
                        .rememberMeCookieName("spring-security-start")) // 保存一个月
//=============================================================
        ;

    }

    private RestAuthenticationFilter restAuthenticationFilter() throws Exception {
        RestAuthenticationFilter filter = new RestAuthenticationFilter(objectMapper);
        filter.setAuthenticationSuccessHandler(jsonAuthenticationSuccessHandler());
        filter.setAuthenticationFailureHandler(jsonAuthenticationFailureHandler());
        // 调用父类 authenticationManager()
        filter.setAuthenticationManager(authenticationManager());
        filter.setFilterProcessesUrl("/authorize/login");
        return filter;
    }

    private LogoutSuccessHandler jsonLogoutSuccessHandler() {
        return (request, response, authentication) -> {
            if (authentication != null && authentication.getDetails() != null) {
                request.getSession().invalidate();
            }
            response.setStatus(HttpStatus.OK.value());
            response.getWriter().println();
            response.getWriter().flush();
            response.getWriter().close();
            log.debug("登出成功");
        };
    }

    private AuthenticationFailureHandler jsonAuthenticationFailureHandler() {
        return (request, response, exception) -> {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding("UTF-8");
            final Map<String, String> errDate = new HashMap<>(16);
            errDate.put("title","认证失败");
            errDate.put("details", exception.getMessage());
            response.getWriter().println(
                    new ObjectMapper().writeValueAsString(errDate)
            );
            response.getWriter().flush();
            response.getWriter().close();
            log.debug("认证失败");
        };
    }

    private AuthenticationSuccessHandler jsonAuthenticationSuccessHandler() {
        return (request, response, authentication) -> {
            response.setStatus(HttpStatus.OK.value());
            response.getWriter().println(
                    new ObjectMapper().writeValueAsString(authentication)
            );
            response.getWriter().flush();
            response.getWriter().close();
            log.debug("认证成功");
        };
    }
//=============================================================
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("eddie")
                .password("{bcrypt}$2a$10$RYV46Kzw56EH0d.qek.7.OOBAdWJql8jdyJypeV0/du5et1L6ShVm")
                .roles("USER","ADMIN")
                .and()
                .withUser("lee")
                .password("{SHA-1}{2mn61IJDaNrHsZ/aX7sk5HU+jedNaoFseoejzmnzu/Y=}5b9505f2babef46787597f8ef6c3af3852379e5e")
                .roles("USER")
        ;
    }

    @Bean
    PasswordEncoder passwordEncoder() {
//      return new MessageDigestPasswordEncoder("MD5");
//	    return new Pbkdf2PasswordEncoder();
//	    return new BCryptPasswordEncoder();
        // 老旧算法是 SHA-1, 希望以后全部替换成 bcrypt
		String idForEncode = "bcrypt";
		Map<String, PasswordEncoder> encoders = new HashMap<>();
		encoders.put(idForEncode, new BCryptPasswordEncoder());
		encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
		return new DelegatingPasswordEncoder(idForEncode, encoders);
	}
	
//=============================================================

    /**
     * 忽略指定${图片,音频,其他静态资源},不走 Security 过滤器检查
     * 会打印:Security filter chain: [] empty (bypassed by security='none')
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().mvcMatchers("/css/**","/error")
                // 会自动查找到 css, js 哪些静态资源在哪里
        .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }
}

2.2.2.2 浏览器测试

刚刚已经解封了表单部分代码,就是为了此刻~ 看招!


2.2.3 验证注解和自定义验证注解

注解说明
@NotNull验证注解的属性值不是空的
@AssertTrue验证注解的属性值是否为真
@Size验证注解的属性值的大小介于属性 min 和 max之间;
可应用于String、Collection、Map 和 Array 属性
@Min验证注解属于的值不小于值属性的值
@Max验证被注解的属性的值不大于值属性的值
@Email验证注解的属性是一个有效的电子邮件地址
@Pattern验证注解的属性是否匹配正则表达式
@NotEmpty验证属性不是空或空;可以应用于String、Collection、Map、或 Array 值
@NotBlank只能应用于文本值,并且验证该属性不是空的或者空白的
@Positive
@PositiveOrZero
适用于数值,并验证它们的严格意思上的正数,或包括 0 在内的正数
@Negative
@NegativeOrZero
适用于数值,并且验证他们是严格意义上的负值,或包括 0 在内的负值
@Past
@PastOrPresent
验证一个日期值是在国企或过去(包括现在)。可应用于日期类型,包括在Java8中新增的日期类型
@Future
@FutureOrProsent
验证一个日期值是在未来,或者说是在未来,包括现在

2.2.3.1 代码环节

Maven 依赖

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

UserDto POJO

@Data
public class UserDto implements Serializable {

	private static final long serialVersionUID = 8293029387499517971L;

	@NotNull // 不为null
	@NotBlank // 不能为空串
	@Size(min = 4, max = 50, message = "用户名长度必须在4-50字符之间")
	private String username;

	@NotNull
	@NotBlank
	@Size(min = 8, max = 20, message = "密码长度必须在4-20字符之间")
	private String password;

	@ValidEmail
	@NotNull
	private String email;

	@NotNull // 不为null
	@NotBlank // 不能为空串
	@Size(min = 4, max = 50, message = "姓名长度必须在4-50字符之间")
	private String name;

	@NotNull
	@NotBlank
	@Size(min = 8, max = 20, message = "密码长度必须在4-20字符之间")
	private String matchPassword;

}

@ValidEmail 自定义注解,替换了简单的 @Email 注解

控制层

@RestController
@RequestMapping("/authorize")
public class UserController {

	@PostMapping("/user/register")
	public UserDto register(@Valid @RequestBody UserDto userDto) {
		return userDto;
	}

}

仿照 validation 注解,搞个玩玩?

自定义邮箱校验注解

@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EmailValidation.class)
@Documented
public @interface ValidEmail {

	String message() default "Invalid Email";

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

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

}

正则表达式校验

public class EmailValidation implements ConstraintValidator<ValidEmail, String> {

	@Override
	public void initialize(ValidEmail constraintAnnotation) {
		// null
	}

	@Override
	public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
		return validateEmail(s);
	}

	/**
	 * @Pattern(regexp = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$")
	 *                 允许英文字母、数字、下划线、英文句号、以及中划线组成
	 * 
	 * @Pattern(regexp =
	 *                 "^[A-Za-z0-9\\u4e00-\\u9fa5]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$")
	 *                 允许汉字、字母、数字,域名只允许英文域名
	 */
	private final static String EMAIL_VALIDATOR = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";

	private boolean validateEmail(String email) {
		Pattern pattern = Pattern.compile(EMAIL_VALIDATOR);
		Matcher matcher = pattern.matcher(email);
		return matcher.matches();
	}
}

效果图

在这里插入图片描述

2.2.4 密码的验证规则和自定义注解和验证器

2.2.4.1 密码的验证规则

  • 密码的验证比较复杂,使用 Passay 框架进行验证
  • 封装验证逻辑在注解中,有效的剥离验证逻辑和业务逻辑
  • 对于2个以上属性的复合验证,可以写一个应用于类的注解

Maven 依赖

<dependency>
    <groupId>org.passay</groupId>
    <artifactId>passay</artifactId>
    <version>1.6.1</version>
</dependency>

自定义密码的验证 - 注解

@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordValidation.class)
@Documented
public @interface ValidPassword {

	String message() default "长度规则:8 - 30 位 | 至少有一个大写字母一个小写字母一个数字一个特殊字符 | 不允许连续 3 个字母,按字母表顺序 | 不允许 3 个连续数字 | 不允许包含空格 ";

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

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

}

自定义密码的验证 - 规则

public class PasswordValidation implements ConstraintValidator<ValidPassword, String> {

	@Override
    public boolean isValid(String password, ConstraintValidatorContext constraintValidatorContext) {
        PasswordValidator validator = new PasswordValidator(Arrays.asList(
                // 长度规则:8 - 30 位
                new LengthRule(8, 30),
                // 至少有一个大写字母
                new CharacterRule(EnglishCharacterData.UpperCase, 1),
                // 至少有一个小写字母
                new CharacterRule(EnglishCharacterData.LowerCase, 1),
                // 至少有一个数字
                new CharacterRule(EnglishCharacterData.Digit, 1),
                // 至少有一个特殊字符
                new CharacterRule(EnglishCharacterData.Special, 1),
                // 不允许连续 3 个字母,按字母表顺序
                // alphabetical is of the form 'abcde', numerical is '34567', qwery is 'asdfg'
                // the false parameter indicates that wrapped sequences are allowed; e.g. 'xyzabc'
                new IllegalSequenceRule(EnglishSequenceData.Alphabetical, 5, false),
                // 不允许 3 个连续数字
                new IllegalSequenceRule(EnglishSequenceData.Numerical, 5, false),
                // 不允许 QWERTY 键盘上的三个连续相邻的按键所代表的字符
                new IllegalSequenceRule(EnglishSequenceData.USQwerty, 5, false),
                // 不允许包含空格
                new WhitespaceRule()));

        RuleResult validate = validator.validate(new PasswordData(password));

        return validate.isValid();
    }

}

效果图
在这里插入图片描述
那么,确认密码能不能做自定义注解方式? 答案: OK~ 看招!

自定义校验确认密码 - 注解

@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordMatchesValidator.class)
@Documented
public @interface PasswordMatches {

    String message() default "两次输入密码不匹配,请重新输入!";

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

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

}

自定义校验确认密码 - 规则

public class PasswordMatchesValidator implements ConstraintValidator<PasswordMatches, UserDto> {

    @Override
    public void initialize(final PasswordMatches constraintAnnotation) { }

    @Override
    public boolean isValid(final UserDto userDto, final ConstraintValidatorContext context) {
        return userDto.getPassword().equals(userDto.getMatchPassword());
    }
}

pojo

@PasswordMatches
@Data
public class UserDto implements Serializable {

	private static final long serialVersionUID = 8293029387499517971L;

	@NotNull // 不为null
	@NotBlank // 不能为空串
	@Size(min = 4, max = 50, message = "用户名长度必须在4-50字符之间")
	private String username;

	@ValidEmail
	@NotNull
	private String email;

	@NotNull // 不为null
	@NotBlank // 不能为空串
	@Size(min = 4, max = 50, message = "姓名长度必须在4-50字符之间")
	private String name;

	@NotNull
//	@NotBlank
//	@Size(min = 8, max = 20, message = "密码长度必须在4-20字符之间")
	@ValidPassword
	private String password;

	@NotNull
//	@NotBlank
//	@Size(min = 8, max = 20, message = "密码长度必须在4-20字符之间")
	private String matchPassword;

}

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

2.2.5 验证消息的国际化

2.2.5.1 Passay 异常的国际化

  • 创建一个消息解析器
  • 配置验证器使用消息解析器
  • 在对应的注解中写消息的键值

WebMvcConfig

@Configuration
//------------------------------------------------------------------
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class WebMvcConfig implements WebMvcConfigurer {

    private final MessageSource messageSource;

    @Bean
    public MessageResolver messageResolver() {
        return new SpringMessageResolver(messageSource);
    }

    @Bean
    public LocalValidatorFactoryBean localValidatorFactoryBean() {
        LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
        bean.setValidationMessageSource(messageSource);
        return bean;
    }
//------------------------------------------------------------------
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 映射
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("/webjars/")
                .resourceChain(false);
        registry.setOrder(1);
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 访问 login 就会访问这个视图
        registry.addViewController("/login").setViewName("login");
        registry.addViewController("/").setViewName("index");
        registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
    }

}

PasswordValidation

//------------------------------------------------------------------
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class PasswordValidation implements ConstraintValidator<ValidPassword, String> {

    private final SpringMessageResolver springMessageResolver;
//------------------------------------------------------------------
    @Override
    public void initialize(ValidPassword constraintAnnotation) {
    }

    @Override
    public boolean isValid(String password, ConstraintValidatorContext constraintValidatorContext) {
//------------------------------------------------------------------
        PasswordValidator validator = new PasswordValidator(springMessageResolver, Arrays.asList(
//------------------------------------------------------------------
                // 长度规则:8 - 30 位
                new LengthRule(8, 30),
                // 至少有一个大写字母
                new CharacterRule(EnglishCharacterData.UpperCase, 1),
                // 至少有一个小写字母
                new CharacterRule(EnglishCharacterData.LowerCase, 1),
                // 至少有一个数字
                new CharacterRule(EnglishCharacterData.Digit, 1),
                // 至少有一个特殊字符
                new CharacterRule(EnglishCharacterData.Special, 1),
                // 不允许连续 3 个字母,按字母表顺序
                // alphabetical is of the form 'abcde', numerical is '34567', qwery is 'asdfg'
                // the false parameter indicates that wrapped sequences are allowed; e.g. 'xyzabc'
                new IllegalSequenceRule(EnglishSequenceData.Alphabetical, 5, false),
                // 不允许 3 个连续数字
                new IllegalSequenceRule(EnglishSequenceData.Numerical, 5, false),
                // 不允许 QWERTY 键盘上的三个连续相邻的按键所代表的字符
                new IllegalSequenceRule(EnglishSequenceData.USQwerty, 5, false),
                // 不允许包含空格
                new WhitespaceRule()));

//------------------------------------------------------------------
        RuleResult validate = validator.validate(new PasswordData(password));
        if (validate.isValid()) {
            return true;
        }
        constraintValidatorContext.disableDefaultConstraintViolation();
        constraintValidatorContext.buildConstraintViolationWithTemplate(
                String.join(",", validator.getMessages(validate))
        ).addConstraintViolation();

        return false;
    }
//------------------------------------------------------------------
}

两个自定义注解

@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordMatchesValidator.class)
@Documented
public @interface PasswordMatches {
//------------------------------------------------------------------
    String message() default "{PasswordMatches.userDto}";
//------------------------------------------------------------------
    Class<?>[] groups() default {};

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

}


@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EmailValidation.class)
@Documented
public @interface ValidEmail {
//------------------------------------------------------------------
	String message() default "{ValidEmail.email}";
//------------------------------------------------------------------
	Class<?>[] groups() default {};

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

}

国际化文件 - messages.properties 点击查看
# Passay properties
HISTORY_VIOLATION=Password matches one of {0} previous passwords.
ILLEGAL_WORD=Password contains the dictionary word '{0}'.
ILLEGAL_WORD_REVERSED=Password contains the reversed dictionary word '{0}'.
ILLEGAL_DIGEST_WORD=Password contains a dictionary word.
ILLEGAL_DIGEST_WORD_REVERSED=Password contains a reversed dictionary word.
ILLEGAL_MATCH=Password matches the illegal pattern '{0}'.
ALLOWED_MATCH=Password must match pattern '{0}'.
ILLEGAL_CHAR=Password {1} the illegal character '{0}'.
ALLOWED_CHAR=Password {1} the illegal character '{0}'.
ILLEGAL_QWERTY_SEQUENCE=Password contains the illegal QWERTY sequence '{0}'.
ILLEGAL_ALPHABETICAL_SEQUENCE=Password contains the illegal alphabetical sequence '{0}'.
ILLEGAL_NUMERICAL_SEQUENCE=Password contains the illegal numerical sequence '{0}'.
ILLEGAL_USERNAME=Password {1} the user id '{0}'.
ILLEGAL_USERNAME_REVERSED=Password {1} the user id '{0}' in reverse.
ILLEGAL_WHITESPACE=Password {1} a whitespace character.
ILLEGAL_NUMBER_RANGE=Password {1} the number '{0}'.
ILLEGAL_REPEATED_CHARS=Password contains {2} sequences of {0} or more repeated characters, but only {1} allowed: {3}.
INSUFFICIENT_UPPERCASE=Password must contain {0} or more uppercase characters.
INSUFFICIENT_LOWERCASE=Password must contain {0} or more lowercase characters.
INSUFFICIENT_ALPHABETICAL=Password must contain {0} or more alphabetical characters.
INSUFFICIENT_DIGIT=Password must contain {0} or more digit characters.
INSUFFICIENT_SPECIAL=Password must contain {0} or more special characters.
INSUFFICIENT_CHARACTERISTICS=Password matches {0} of {2} character rules, but {1} are required.
INSUFFICIENT_COMPLEXITY=Password meets {1} complexity rules, but {2} are required.
INSUFFICIENT_COMPLEXITY_RULES=No rules have been configured for a password of length {0}.
SOURCE_VIOLATION=Password cannot be the same as your {0} password.
TOO_LONG=Password must be no more than {1} characters in length.
TOO_SHORT=Password must be {0} or more characters in length.
TOO_MANY_OCCURRENCES=Password contains {1} occurrences of the character '{0}', but at most {2} are allowed.

jakarta.validation.constraints.AssertFalse.message     = must be false
jakarta.validation.constraints.AssertTrue.message      = must be true
jakarta.validation.constraints.DecimalMax.message      = must be less than ${inclusive == true ? 'or equal to ' : ''}{value}
jakarta.validation.constraints.DecimalMin.message      = must be greater than ${inclusive == true ? 'or equal to ' : ''}{value}
jakarta.validation.constraints.Digits.message          = numeric value out of bounds (<{integer} digits>.<{fraction} digits> expected)
jakarta.validation.constraints.Email.message           = must be a well-formed email address
jakarta.validation.constraints.Future.message          = must be a future date
jakarta.validation.constraints.FutureOrPresent.message = must be a date in the present or in the future
jakarta.validation.constraints.Max.message             = must be less than or equal to {value}
jakarta.validation.constraints.Min.message             = must be greater than or equal to {value}
jakarta.validation.constraints.Negative.message        = must be less than 0
jakarta.validation.constraints.NegativeOrZero.message  = must be less than or equal to 0
jakarta.validation.constraints.NotBlank.message        = must not be blank
jakarta.validation.constraints.NotEmpty.message        = must not be empty
jakarta.validation.constraints.NotNull.message         = must not be null
jakarta.validation.constraints.Null.message            = must be null
jakarta.validation.constraints.Past.message            = must be a past date
jakarta.validation.constraints.PastOrPresent.message   = must be a date in the past or in the present
jakarta.validation.constraints.Pattern.message         = must match "{regexp}"
jakarta.validation.constraints.Positive.message        = must be greater than 0
jakarta.validation.constraints.PositiveOrZero.message  = must be greater than or equal to 0
jakarta.validation.constraints.Size.message            = size must be between {min} and {max}

org.hibernate.validator.constraints.CreditCardNumber.message        = invalid credit card number
org.hibernate.validator.constraints.Currency.message                = invalid currency (must be one of {value})
org.hibernate.validator.constraints.EAN.message                     = invalid {type} barcode
org.hibernate.validator.constraints.Email.message                   = not a well-formed email address
org.hibernate.validator.constraints.ISBN.message                    = invalid ISBN
org.hibernate.validator.constraints.Length.message                  = length must be between {min} and {max}
org.hibernate.validator.constraints.CodePointLength.message         = length must be between {min} and {max}
org.hibernate.validator.constraints.LuhnCheck.message               = the check digit for ${validatedValue} is invalid, Luhn Modulo 10 checksum failed
org.hibernate.validator.constraints.Mod10Check.message              = the check digit for ${validatedValue} is invalid, Modulo 10 checksum failed
org.hibernate.validator.constraints.Mod11Check.message              = the check digit for ${validatedValue} is invalid, Modulo 11 checksum failed
org.hibernate.validator.constraints.ModCheck.message                = the check digit for ${validatedValue} is invalid, {modType} checksum failed
org.hibernate.validator.constraints.Normalized.message              = must be normalized
org.hibernate.validator.constraints.NotBlank.message                = may not be empty
org.hibernate.validator.constraints.NotEmpty.message                = may not be empty
org.hibernate.validator.constraints.ParametersScriptAssert.message  = script expression "{script}" didn't evaluate to true
org.hibernate.validator.constraints.Range.message                   = must be between {min} and {max}
org.hibernate.validator.constraints.ScriptAssert.message            = script expression "{script}" didn't evaluate to true
org.hibernate.validator.constraints.UniqueElements.message          = must only contain unique elements
org.hibernate.validator.constraints.URL.message                     = must be a valid URL

org.hibernate.validator.constraints.br.CNPJ.message                 = invalid Brazilian corporate taxpayer registry number (CNPJ)
org.hibernate.validator.constraints.br.CPF.message                  = invalid Brazilian individual taxpayer registry number (CPF)
org.hibernate.validator.constraints.br.TituloEleitoral.message      = invalid Brazilian Voter ID card number

org.hibernate.validator.constraints.pl.REGON.message                = invalid Polish Taxpayer Identification Number (REGON)
org.hibernate.validator.constraints.pl.NIP.message                  = invalid VAT Identification Number (NIP)
org.hibernate.validator.constraints.pl.PESEL.message                = invalid Polish National Identification Number (PESEL)

org.hibernate.validator.constraints.time.DurationMax.message        = must be shorter than${inclusive == true ? ' or equal to' : ''}${days == 0 ? '' : days == 1 ? ' 1 day' : ' ' += days += ' days'}${hours == 0 ? '' : hours == 1 ? ' 1 hour' : ' ' += hours += ' hours'}${minutes == 0 ? '' : minutes == 1 ? ' 1 minute' : ' ' += minutes += ' minutes'}${seconds == 0 ? '' : seconds == 1 ? ' 1 second' : ' ' += seconds += ' seconds'}${millis == 0 ? '' : millis == 1 ? ' 1 milli' : ' ' += millis += ' millis'}${nanos == 0 ? '' : nanos == 1 ? ' 1 nano' : ' ' += nanos += ' nanos'}
org.hibernate.validator.constraints.time.DurationMin.message        = must be longer than${inclusive == true ? ' or equal to' : ''}${days == 0 ? '' : days == 1 ? ' 1 day' : ' ' += days += ' days'}${hours == 0 ? '' : hours == 1 ? ' 1 hour' : ' ' += hours += ' hours'}${minutes == 0 ? '' : minutes == 1 ? ' 1 minute' : ' ' += minutes += ' minutes'}${seconds == 0 ? '' : seconds == 1 ? ' 1 second' : ' ' += seconds += ' seconds'}${millis == 0 ? '' : millis == 1 ? ' 1 milli' : ' ' += millis += ' millis'}${nanos == 0 ? '' : nanos == 1 ? ' 1 nano' : ' ' += nanos += ' nanos'}

ValidEmail.email=Invalid Email
PasswordMatches.userDto=Passwords do not match

login.page.title=Login
login.page.logout.msg=You have logged out.
login.page.bad-credential=Username or password is wrong
login.page.form.username=Username
login.page.form.password=Password
login.page.form.submit=Login
index.page.menu.sign-out=Sign Out
login.page.form.remember-me=Remember me


国际化文件 - messages_zh_CN.properties 点击查看
# Passay 属性
HISTORY_VIOLATION=密码和您最近用过的 {0} 个密码之一重复。
ILLEGAL_WORD=密码包含了黑名单字典中的词 {0}。
ILLEGAL_WORD_REVERSED=密码包含了保留字典中的词 {0}。
ILLEGAL_DIGEST_WORD=密码包含了字典中的词。
ILLEGAL_DIGEST_WORD_REVERSED=密码包含了保留字典中的词。
ILLEGAL_MATCH=密码匹配了非法结构 {0}。
ALLOWED_MATCH=密码必须要匹配结构 {0}。
ILLEGAL_CHAR=密码 {1} 非法字符 {0}。
ALLOWED_CHAR=密码 {1} 非法字符 {0}。
ILLEGAL_QWERTY_SEQUENCE=密码包含非法的QWERTY序列 {0}。
ILLEGAL_ALPHABETICAL_SEQUENCE=密码包含非法的字母序列 {0}。
ILLEGAL_NUMERICAL_SEQUENCE=密码包含非法的数字序列 {0}。
ILLEGAL_USERNAME=密码 {1} 用户 id {0}。
ILLEGAL_USERNAME_REVERSED=密码 {1} 倒序的用户 id {0}。
ILLEGAL_WHITESPACE=密码 {1} 空格。
ILLEGAL_NUMBER_RANGE=密码 {1} 数字 {0}.
ILLEGAL_REPEATED_CHARS=密码中包含 {2} 序列 {0} 的一个或多个重复字符, 但仅允许 {1} 个: {3}。
INSUFFICIENT_UPPERCASE=密码中必须包含至少 {0} 个大写字母。
INSUFFICIENT_LOWERCASE=密码中必须包含至少 {0} 个小写字母。
INSUFFICIENT_ALPHABETICAL=密码中必须包含至少 {0} 个字母。
INSUFFICIENT_DIGIT=密码中必须包含至少 {0} 个数字。
INSUFFICIENT_SPECIAL=密码中必须包含至少 {0} 个特殊字符。
INSUFFICIENT_CHARACTERISTICS=密码匹配了 {0} of {2} 字符规则, 但只允许 {1} 个。
INSUFFICIENT_COMPLEXITY=密码符合了 {1} 个复杂规则, 但需要符合 {2} 个。
INSUFFICIENT_COMPLEXITY_RULES=对于密码长度 {0},没有配置规则。
SOURCE_VIOLATION=密码不能和之前的 {0} 个历史密码相同。
TOO_LONG=密码长度不能超过 {1} 个字符。
TOO_SHORT=密码长度不能少于 {0} 个字符。
TOO_MANY_OCCURRENCES=密码包含 {1} 个 {0}, 但是至多只允许 {2} 个。

jakarta.validation.constraints.AssertFalse.message     = \u53ea\u80fd\u4e3afalse
jakarta.validation.constraints.AssertTrue.message      = \u53ea\u80fd\u4e3atrue
jakarta.validation.constraints.DecimalMax.message      = \u5fc5\u987b\u5c0f\u4e8e\u6216\u7b49\u4e8e{value}
jakarta.validation.constraints.DecimalMin.message      = \u5fc5\u987b\u5927\u4e8e\u6216\u7b49\u4e8e{value}
jakarta.validation.constraints.Digits.message          = \u6570\u5b57\u7684\u503c\u8d85\u51fa\u4e86\u5141\u8bb8\u8303\u56f4(\u53ea\u5141\u8bb8\u5728{integer}\u4f4d\u6574\u6570\u548c{fraction}\u4f4d\u5c0f\u6570\u8303\u56f4\u5185)
jakarta.validation.constraints.Email.message           = \u4e0d\u662f\u4e00\u4e2a\u5408\u6cd5\u7684\u7535\u5b50\u90ae\u4ef6\u5730\u5740
jakarta.validation.constraints.Future.message          = \u9700\u8981\u662f\u4e00\u4e2a\u5c06\u6765\u7684\u65f6\u95f4
jakarta.validation.constraints.FutureOrPresent.message = \u9700\u8981\u662f\u4e00\u4e2a\u5c06\u6765\u6216\u73b0\u5728\u7684\u65f6\u95f4
jakarta.validation.constraints.Max.message             = \u6700\u5927\u4e0d\u80fd\u8d85\u8fc7{value}
jakarta.validation.constraints.Min.message             = \u6700\u5c0f\u4e0d\u80fd\u5c0f\u4e8e{value}
jakarta.validation.constraints.Negative.message        = \u5fc5\u987b\u662f\u8d1f\u6570
jakarta.validation.constraints.NegativeOrZero.message  = \u5fc5\u987b\u662f\u8d1f\u6570\u6216\u96f6
jakarta.validation.constraints.NotBlank.message        = \u4e0d\u80fd\u4e3a\u7a7a
jakarta.validation.constraints.NotEmpty.message        = \u4e0d\u80fd\u4e3a\u7a7a
jakarta.validation.constraints.NotNull.message         = \u4e0d\u80fd\u4e3anull
jakarta.validation.constraints.Null.message            = \u5fc5\u987b\u4e3anull
jakarta.validation.constraints.Past.message            = \u9700\u8981\u662f\u4e00\u4e2a\u8fc7\u53bb\u7684\u65f6\u95f4
jakarta.validation.constraints.PastOrPresent.message   = \u9700\u8981\u662f\u4e00\u4e2a\u8fc7\u53bb\u6216\u73b0\u5728\u7684\u65f6\u95f4
jakarta.validation.constraints.Pattern.message         = \u9700\u8981\u5339\u914d\u6b63\u5219\u8868\u8fbe\u5f0f"{regexp}"
jakarta.validation.constraints.Positive.message        = \u5fc5\u987b\u662f\u6b63\u6570
jakarta.validation.constraints.PositiveOrZero.message  = \u5fc5\u987b\u662f\u6b63\u6570\u6216\u96f6
jakarta.validation.constraints.Size.message            = \u4e2a\u6570\u5fc5\u987b\u5728{min}\u548c{max}\u4e4b\u95f4

org.hibernate.validator.constraints.CreditCardNumber.message        = \u4e0d\u5408\u6cd5\u7684\u4fe1\u7528\u5361\u53f7\u7801
org.hibernate.validator.constraints.Currency.message                = \u4e0d\u5408\u6cd5\u7684\u8d27\u5e01 (\u5fc5\u987b\u662f{value}\u5176\u4e2d\u4e4b\u4e00)
org.hibernate.validator.constraints.EAN.message                     = \u4e0d\u5408\u6cd5\u7684{type}\u6761\u5f62\u7801
org.hibernate.validator.constraints.Email.message                   = \u4e0d\u662f\u4e00\u4e2a\u5408\u6cd5\u7684\u7535\u5b50\u90ae\u4ef6\u5730\u5740
org.hibernate.validator.constraints.Length.message                  = \u957f\u5ea6\u9700\u8981\u5728{min}\u548c{max}\u4e4b\u95f4
org.hibernate.validator.constraints.CodePointLength.message         = \u957f\u5ea6\u9700\u8981\u5728{min}\u548c{max}\u4e4b\u95f4
org.hibernate.validator.constraints.LuhnCheck.message               = ${validatedValue}\u7684\u6821\u9a8c\u7801\u4e0d\u5408\u6cd5, Luhn\u6a2110\u6821\u9a8c\u548c\u4e0d\u5339\u914d
org.hibernate.validator.constraints.Mod10Check.message              = ${validatedValue}\u7684\u6821\u9a8c\u7801\u4e0d\u5408\u6cd5, \u6a2110\u6821\u9a8c\u548c\u4e0d\u5339\u914d
org.hibernate.validator.constraints.Mod11Check.message              = ${validatedValue}\u7684\u6821\u9a8c\u7801\u4e0d\u5408\u6cd5, \u6a2111\u6821\u9a8c\u548c\u4e0d\u5339\u914d
org.hibernate.validator.constraints.ModCheck.message                = ${validatedValue}\u7684\u6821\u9a8c\u7801\u4e0d\u5408\u6cd5, {modType}\u6821\u9a8c\u548c\u4e0d\u5339\u914d
org.hibernate.validator.constraints.NotBlank.message                = \u4e0d\u80fd\u4e3a\u7a7a
org.hibernate.validator.constraints.NotEmpty.message                = \u4e0d\u80fd\u4e3a\u7a7a
org.hibernate.validator.constraints.ParametersScriptAssert.message  = \u6267\u884c\u811a\u672c\u8868\u8fbe\u5f0f"{script}"\u6ca1\u6709\u8fd4\u56de\u671f\u671b\u7ed3\u679c
org.hibernate.validator.constraints.Range.message                   = \u9700\u8981\u5728{min}\u548c{max}\u4e4b\u95f4
org.hibernate.validator.constraints.ScriptAssert.message            = \u6267\u884c\u811a\u672c\u8868\u8fbe\u5f0f"{script}"\u6ca1\u6709\u8fd4\u56de\u671f\u671b\u7ed3\u679c
org.hibernate.validator.constraints.URL.message                     = \u9700\u8981\u662f\u4e00\u4e2a\u5408\u6cd5\u7684URL

org.hibernate.validator.constraints.time.DurationMax.message        = \u5fc5\u987b\u5c0f\u4e8e${inclusive == true ? '\u6216\u7b49\u4e8e' : ''}${days == 0 ? '' : days += '\u5929'}${hours == 0 ? '' : hours += '\u5c0f\u65f6'}${minutes == 0 ? '' : minutes += '\u5206\u949f'}${seconds == 0 ? '' : seconds += '\u79d2'}${millis == 0 ? '' : millis += '\u6beb\u79d2'}${nanos == 0 ? '' : nanos += '\u7eb3\u79d2'}
org.hibernate.validator.constraints.time.DurationMin.message        = \u5fc5\u987b\u5927\u4e8e${inclusive == true ? '\u6216\u7b49\u4e8e' : ''}${days == 0 ? '' : days += '\u5929'}${hours == 0 ? '' : hours += '\u5c0f\u65f6'}${minutes == 0 ? '' : minutes += '\u5206\u949f'}${seconds == 0 ? '' : seconds += '\u79d2'}${millis == 0 ? '' : millis += '\u6beb\u79d2'}${nanos == 0 ? '' : nanos += '\u7eb3\u79d2'}

ValidEmail.email=非法电子邮件地址
PasswordMatches.userDto=密码输入不一致

login.page.title=登录
login.page.logout.msg=您已退出登录
login.page.bad-credential=用户名或密码不正确
login.page.form.username=用户名
login.page.form.password=密码
login.page.form.submit=登录
index.page.menu.sign-out=退出登录
login.page.form.remember-me=记住我

效果图

在这里插入图片描述

2.2.6 异常处理

2.2.6.1 异常的统一处理

Maven 依赖 problem

 <dependency>
     <groupId>org.zalando</groupId>
     <artifactId>problem-spring-web-starter</artifactId>
     <version>0.26.1</version>
 </dependency>

ExceptionHandler

@ControllerAdvice
public class ExceptionHandler implements ProblemHandling {

	/**
	 * 生产不建议开启: <br>
	 * 是否将堆栈中的错误信息返回
	 * 
	 * @return boolean
	 */
    @Override
    public boolean isCausalChainsEnabled() {
        return true;
    }
}

SecurityExceptionHandler

@ControllerAdvice
public class SecurityExceptionHandler implements SecurityAdviceTrait {

}

SecurityConfig

@Slf4j
// (debug = true) 请勿在生产系统中使用
@EnableWebSecurity(debug = true)
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
//------------------------------------------------------------------
@Order(99)
@Import(SecurityProblemSupport.class)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final ObjectMapper objectMapper;

    private final SecurityProblemSupport securityProblemSupport;

	// 在父类的352行
	@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .exceptionHandling(exceptionHandling -> exceptionHandling
                        .authenticationEntryPoint(securityProblemSupport)
                        .accessDeniedHandler(securityProblemSupport))
//------------------------------------------------------------------
                // 所有路径都需要认证
                .authorizeRequests(r -> r
                        .antMatchers("/authorize/**").permitAll()
                        .antMatchers("/admin/**").hasRole("ADMIN")
                        .antMatchers("/api/**").hasRole("USER")
                        .anyRequest().authenticated())
                // restAuthenticationFilter() 替代 UsernamePasswordAuthenticationFilter.class 过滤器
                .addFilterAt(restAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                // 这几个路径不使用csrf token
                .csrf(c -> c.ignoringAntMatchers("/authorize/**","/admin/**","/api/**"))
                // 不添加 permitAll() 的话,就会一直“重定向”
                .formLogin(f -> f.loginPage("/login")
//                        .usernameParameter("username1")  // 指定username参数为username1,但需要前端配合传参也是 username1
                        // 登录成功,返回json
                        .defaultSuccessUrl("/")
                        .successHandler(jsonAuthenticationSuccessHandler())
                        // 登录失败,返回json
                        .failureHandler(jsonAuthenticationFailureHandler())
                        .permitAll())
//                 显示浏览器对话框,需要禁用 CSRF ,或添加路径到忽略列表
                .httpBasic(Customizer.withDefaults())
                .csrf(Customizer.withDefaults())
//                 index.html  perform_logout 45行;  "/" 不能少,不然跳转404
                .logout(l -> l.logoutUrl("/perform_logout")
                        // 登出成功,返回json
//                        .logoutSuccessUrl("/login")
                        .logoutSuccessHandler(jsonLogoutSuccessHandler())
                        .deleteCookies("JSESSIONID")
                )
                .rememberMe(r -> r.tokenValiditySeconds(30 * 24 * 3600)
                        .rememberMeCookieName("spring-security-start")) // 保存一个月
        ;

    }

    private RestAuthenticationFilter restAuthenticationFilter() throws Exception {
        RestAuthenticationFilter filter = new RestAuthenticationFilter(objectMapper);
        filter.setAuthenticationSuccessHandler(jsonAuthenticationSuccessHandler());
        filter.setAuthenticationFailureHandler(jsonAuthenticationFailureHandler());
        // 调用父类 authenticationManager()
        filter.setAuthenticationManager(authenticationManager());
        filter.setFilterProcessesUrl("/authorize/login");
        return filter;
    }

    private LogoutSuccessHandler jsonLogoutSuccessHandler() {
        return (request, response, authentication) -> {
            if (authentication != null && authentication.getDetails() != null) {
                request.getSession().invalidate();
            }
            response.setStatus(HttpStatus.OK.value());
            response.getWriter().println();
            response.getWriter().flush();
            response.getWriter().close();
            log.debug("登出成功");
        };
    }

    private AuthenticationFailureHandler jsonAuthenticationFailureHandler() {
        return (request, response, exception) -> {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding("UTF-8");
            final Map<String, String> errDate = new HashMap<>(16);
            errDate.put("title","认证失败");
            errDate.put("details", exception.getMessage());
            response.getWriter().println(
                    new ObjectMapper().writeValueAsString(errDate)
            );
            response.getWriter().flush();
            response.getWriter().close();
            log.debug("认证失败");
        };
    }

    private AuthenticationSuccessHandler jsonAuthenticationSuccessHandler() {
        return (request, response, authentication) -> {
            response.setStatus(HttpStatus.OK.value());
            response.getWriter().println(
                    new ObjectMapper().writeValueAsString(authentication)
            );
            response.getWriter().flush();
            response.getWriter().close();
            log.debug("认证成功");
        };
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("eddie")
                .password("{bcrypt}$2a$10$RYV46Kzw56EH0d.qek.7.OOBAdWJql8jdyJypeV0/du5et1L6ShVm")
                .roles("USER","ADMIN")
                .and()
                .withUser("lee")
                .password("{SHA-1}{2mn61IJDaNrHsZ/aX7sk5HU+jedNaoFseoejzmnzu/Y=}5b9505f2babef46787597f8ef6c3af3852379e5e")
                .roles("USER")
        ;
    }

    @Bean
    PasswordEncoder passwordEncoder() {
//      return new MessageDigestPasswordEncoder("MD5");
//	    return new Pbkdf2PasswordEncoder();
//	    return new BCryptPasswordEncoder();
        // 老旧算法是 SHA-1, 希望以后全部替换成 bcrypt
		String idForEncode = "bcrypt";
		Map<String, PasswordEncoder> encoders = new HashMap<>();
		encoders.put(idForEncode, new BCryptPasswordEncoder());
		encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
		return new DelegatingPasswordEncoder(idForEncode, encoders);
	}

    /**
     * 忽略指定${图片,音频,其他静态资源},不走 Security 过滤器检查
     * 会打印:Security filter chain: [] empty (bypassed by security='none')
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().mvcMatchers("/css/**","/error")
                // 会自动查找到 css, js 哪些静态资源在哪里
        .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }
}

application.yml - 如果出现中文乱码,可加此配置

server:
  servlet:
    encoding:
      force: true

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

2.2.7 多个安全配置共存

从 SecurityConfig 表单和登出、记住我的功能抽取出来, 废话不多说 这次直接贴最终代码

2.2.7.1 SecurityConfig

package com.example.edcode.config;

import com.example.edcode.security.filter.RestAuthenticationFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.MessageDigestPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.zalando.problem.spring.web.advice.security.SecurityProblemSupport;

import java.util.HashMap;
import java.util.Map;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 */
@Slf4j
// (debug = true) 请勿在生产系统中使用
@EnableWebSecurity(debug = true)
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Order(99)
// 一个类库提供了很多bean,在我们这个配置里面我只关心其中一个或者几个,那我就使用 import 来把需要的列出来,依赖注入的时候,如果发现这个bean没有就会明确告诉你,而不是说过一大坨bean都找不到
@Import(SecurityProblemSupport.class)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final ObjectMapper objectMapper;

    private final SecurityProblemSupport securityProblemSupport;

	// 在父类的352行
	@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .requestMatchers(req -> req.mvcMatchers("/api/**", "/admin/**", "/authorize/**"))

                .sessionManagement(sessionManagement -> sessionManagement
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS))

                .exceptionHandling(exceptionHandling -> exceptionHandling
                        .authenticationEntryPoint(securityProblemSupport)
                        .accessDeniedHandler(securityProblemSupport))

                // 所有路径都需要认证
                .authorizeRequests(r -> r
                        .antMatchers("/authorize/**").permitAll()
                        .antMatchers("/admin/**").hasRole("ADMIN")
                        .antMatchers("/api/**").hasRole("USER")
                        .anyRequest().authenticated())

                // restAuthenticationFilter() 替代 UsernamePasswordAuthenticationFilter.class 过滤器
                .addFilterAt(restAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)

                // 这几个路径不使用csrf token
                .csrf(c -> c.ignoringAntMatchers("/authorize/**","/admin/**","/api/**"))

                // 显示浏览器对话框,需要禁用 CSRF ,或添加路径到忽略列表
                .httpBasic(AbstractHttpConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable)
        ;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("eddie")
                .password("{bcrypt}$2a$10$RYV46Kzw56EH0d.qek.7.OOBAdWJql8jdyJypeV0/du5et1L6ShVm")
                .roles("USER","ADMIN")
                .and()
                .withUser("lee")
                .password("{SHA-1}{2mn61IJDaNrHsZ/aX7sk5HU+jedNaoFseoejzmnzu/Y=}5b9505f2babef46787597f8ef6c3af3852379e5e")
                .roles("USER")
        ;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web
                .ignoring()
                .antMatchers( "/error/**");
    }

    @Bean
    PasswordEncoder passwordEncoder() {
//      return new MessageDigestPasswordEncoder("MD5");
//	    return new Pbkdf2PasswordEncoder();
//	    return new BCryptPasswordEncoder();
        // 老旧算法是 SHA-1, 希望以后全部替换成 bcrypt
		String idForEncode = "bcrypt";
		Map<String, PasswordEncoder> encoders = new HashMap<>();
		encoders.put(idForEncode, new BCryptPasswordEncoder());
		encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
		return new DelegatingPasswordEncoder(idForEncode, encoders);
	}

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

    private RestAuthenticationFilter restAuthenticationFilter() throws Exception {
        RestAuthenticationFilter filter = new RestAuthenticationFilter(objectMapper);
        filter.setAuthenticationSuccessHandler(jsonAuthenticationSuccessHandler());
        filter.setAuthenticationFailureHandler(jsonAuthenticationFailureHandler());
        filter.setAuthenticationManager(authenticationManager());
        filter.setFilterProcessesUrl("/authorize/login");
        return filter;
    }

    private AuthenticationSuccessHandler jsonAuthenticationSuccessHandler() {
        return (request, response, authentication) -> {
            response.setStatus(HttpStatus.OK.value());
            response.getWriter().println(
                    new ObjectMapper().writeValueAsString(authentication)
            );
            response.getWriter().flush();
            response.getWriter().close();
            log.debug("认证成功");
        };
    }

    private AuthenticationFailureHandler jsonAuthenticationFailureHandler() {
        return (request, response, exception) -> {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding("UTF-8");
            final Map<String, String> errDate = new HashMap<>(16);
            errDate.put("title","认证失败");
            errDate.put("details", exception.getMessage());
            response.getWriter().println(
                    new ObjectMapper().writeValueAsString(errDate)
            );
            response.getWriter().flush();
            response.getWriter().close();
            log.debug("认证失败");
        };
    }

    private LogoutSuccessHandler jsonLogoutSuccessHandler() {
        return (request, response, authentication) -> {
            if (authentication != null && authentication.getDetails() != null) {
                request.getSession().invalidate();
            }
            response.setStatus(HttpStatus.OK.value());
            response.getWriter().println();
            response.getWriter().flush();
            response.getWriter().close();
            log.debug("登出成功");
        };
    }
}

2.2.7.2 LoginSecurityConfig

package com.example.edcode.config;

import com.example.edcode.security.filter.RestAuthenticationFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import java.util.HashMap;
import java.util.Map;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 */
@Slf4j
@Order(100)
@Configuration
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 不添加 permitAll() 的话,就会一直“重定向”
                .formLogin(f -> f.loginPage("/login")
                        .failureUrl("/login?error")
                        .defaultSuccessUrl("/")
                        .permitAll())
                //  index.html  perform_logout 45行;  "/" 不能少,不然跳转404
                .logout(l -> l.logoutUrl("/perform_logout")
                        .logoutSuccessUrl("/login")
                )
                .rememberMe(r -> r.tokenValiditySeconds(30 * 24 * 3600)
                        .rememberMeCookieName("spring-security-start")) // 保存一个月
                .authorizeRequests(r -> r.anyRequest().authenticated())
                ;
    }

    /**
     * 忽略指定${图片,音频,其他静态资源},不走 Security 过滤器检查
     * 会打印:Security filter chain: [] empty (bypassed by security='none')
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }
}

2.2.7.3 效果图

(一)浏览器方式登录

在这里插入图片描述
(二)Api方式请求接口

在这里插入图片描述