@[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 浏览器测试
刚刚已经解封了表单部分代码,就是为了此刻~ 看招!
- 访问 http://localhost:8080/login
- 使用 eddie / 123456 登录
- 登录成功后,再次访问 http://localhost:8080 选择退出
- 访问 http://localhost:8080/login
- 使用 lee/ 123456 登录
- 登录成功后,再次访问 http://localhost:8080 选择退出
- 以上均为可行
2.2.3 验证注解和自定义验证注解
注解 | 说明 |
---|---|
@NotNull | 验证注解的属性值不是空的 |
@AssertTrue | 验证注解的属性值是否为真 |
@Size | 验证注解的属性值的大小介于属性 min 和 max之间; 可应用于String、Collection、Map 和 Array 属性 |
@Min | 验证注解属于的值不小于值属性的值 |
@Max | 验证被注解的属性的值不大于值属性的值 |
验证注解的属性是一个有效的电子邮件地址 | |
@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方式请求接口