实现自定义表单登录
1.实现自定义UserDetailsService
需要重写 loadUserByUsername 方法,参数是用户输入的用户名。在此,我们可以实现自定义的登录逻辑。
@Component(value = "myUserDetailService")
public class MyUserDetailServiceImpl implements UserDetailsService {
/**
* 用户Service
*/
private final UserService userService;
/**
* 用户角色Service
*/
private final UserRoleService userRoleService;
/**
* 角色Service
*/
private final RoleService roleService;
/**
* 角色权限Service
*/
private final RoleAuthorityService roleAuthorityService;
/**
* 权限Service
*/
private final AuthorityService authorityService;
@Autowired
public MyUserDetailServiceImpl(UserService userService, UserRoleService userRoleService,
RoleService roleService, RoleAuthorityService roleAuthorityService,
AuthorityService authorityService) {
this.userService = userService;
this.userRoleService = userRoleService;
this.roleService = roleService;
this.roleAuthorityService = roleAuthorityService;
this.authorityService = authorityService;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//这里可以可以通过username(登录时输入的用户名)然后到数据库中找到对应的用户信息,并构建成我们自己的UserInfo来返回。
User user = userService.findUserByName(username);
if (ObjectUtils.isEmpty(user)) {
throw new UsernameNotFoundException("userName not found");
}
//角色授权:授权代码需要加ROLE_前缀,controller上使用时不要加前缀
//权限授权:设置和使用时,名称保持一至即可
List<Integer> roleIdList = userRoleService.findRoleIdsByUserId(user.getUserId());
List<Role> roleList = roleService.findByRoleIdIn(roleIdList);
String role = "";
StringBuilder roleSb = new StringBuilder();
roleList.forEach(e -> roleSb.append(",").append("ROLE_").append(e.getRoleCode()));
if (roleSb.length() > 0) {
role = roleSb.substring(1);
}
List<Integer> authorityIdList = roleAuthorityService.getAuthorityIdsByRoleIds(roleIdList);
List<Authority> authorityList = authorityService.getAuthority(authorityIdList);
String authority = "";
StringBuilder authoritySb = new StringBuilder();
authorityList.forEach(e -> authoritySb.append(",").append(e.getAuthorityCode()));
if (authoritySb.length() > 0) {
authority = authoritySb.substring(1);
}
return new UserDto(user.getUserId(), user.getUserName(), user.getUserPassword(), user.getUserSalt(),
role + authority, true, true, true, true);
}
}
输入账号密码后,security会调用此方法,通过用户名查找此用户是否存在,最后面我没有直接返回security的User,而是返回了自定义的实体类,代码如下:
public class UserDto extends BaseDto implements UserDetails, Serializable {
private static final long serialVersionUID = 1L;
/** 用户ID */
@Getter
private Integer userId;
/** 用户名 */
private String username;
/** 用户密码 */
private String password;
/** 加密盐 */
@Getter
private String salt;
/** 角色 */
private String role;
/** 账户是否过期 */
private boolean accountNonExpired;
/** 账户是否被锁定 */
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
public UserDto(Integer userId, String username, String password, String salt, String role,
boolean accountNonExpired, boolean accountNonLocked, boolean credentialsNonExpired, boolean enabled) {
this.userId = userId;
this.username = username;
this.password = password;
this.salt = salt;
this.role = role;
this.accountNonExpired = accountNonExpired;
this.accountNonLocked = accountNonLocked;
this.credentialsNonExpired = credentialsNonExpired;
this.enabled = enabled;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return AuthorityUtils.commaSeparatedStringToAuthorityList(role);
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
2.Security全局配置
@Configuration
@EnableWebSecurity //开启 Security 服务
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启全局 Securtiy 注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 表单认证配置
*/
private final FormAuthenticationConfig formAuthenticationConfig;
/**
* 验证用户Service
*/
private final UserDetailsService userDetailsService;
@Autowired
public SecurityConfig(FormAuthenticationConfig formAuthenticationConfig, @Qualifier(value = "myUserDetailService") UserDetailsService userDetailsService) {
this.formAuthenticationConfig = formAuthenticationConfig;
this.userDetailsService = userDetailsService;
}
/**
* 登录配置
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//不需要拦截的路径
.antMatchers("/authentication/require").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
//表单登录配置
formAuthenticationConfig.configure(http);
}
/**
* 权限校验配置
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//使用自定义userDetailsService进行登录校验
auth.userDetailsService(userDetailsService)
//加密工具
.passwordEncoder(passwordEncoder());
}
/**
* 加密工具(指定了密码的加密方式(5.0 版本强制要求设置))
*
* @return PasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder(){
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
};
}
}
2.1.formAuthenticationConfig
在这里将表单登录配置抽取出来,单独放在一个类里
@Component
public class FormAuthenticationConfig {
private final AuthenticationSuccessHandler authenticationSuccessHandler;
private final AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
public FormAuthenticationConfig(@Qualifier(value = "myAuthenticationSuccessHandler") AuthenticationSuccessHandler authenticationSuccessHandler,
@Qualifier(value = "myAuthenticationFailureHandler") AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationSuccessHandler = authenticationSuccessHandler;
this.authenticationFailureHandler = authenticationFailureHandler;
}
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
//当请求需要身份认证时,默认跳转的url
.loginPage("/authentication/require")
//默认的用户名密码登录请求处理url
.loginProcessingUrl("/authentication/form")
//登录成功处理器
.successHandler(authenticationSuccessHandler)
//登录失败处理器
.failureHandler(authenticationFailureHandler);
}
}
2.2.登录成功处理器
@Slf4j
@Component("myAuthenticationSuccessHandler")
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private final ObjectMapper objectMapper;
public MyAuthenticationSuccessHandler(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
log.info("登录成功");
//返回数据
response.setContentType("application/json;charset=UTF-8");
String type = authentication.getClass().getSimpleName();
ResultVo resultVo = ResultVo.builder().status(200).msg("登陆成功!").data(type).build();
response.getWriter().write(objectMapper.writeValueAsString(resultVo));
}
}
2.3. 登陆失败处理器
@Slf4j
@Component("myAuthenticationFailureHandler")
public class MyAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private final ObjectMapper objectMapper;
public MyAuthenctiationFailureHandler(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException {
logger.info("登录失败");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
ResultVo resultVo = ResultVo.builder().status(403).msg("登陆失败!").data(exception.getMessage()).build();
response.getWriter().write(objectMapper.writeValueAsString(resultVo));
}
}
3.登录验证
在这里需要打开一个工具postman对项目进行验证
3.1.未登录测试
当用户未登录时访问项目接口,会跳转到自定义配置的路径上,因为这里并没有配置此路径接口,所有会报404错误。
3.2.登录错误测试
紧接着,向/authentication/form发起登录请求,输入一个错误的密码,此时会调用登录失败处理器,返回错误信息。
3.3.登陆成功测试
最后,输入正确的用户和密码,此时会调用登录成功处理器,返回成功信息。
此时,再次访问项目接口,可以看到,访问成功!
4.关于用户名和密码
表单登录,默认的用户名和密码分别是username和password,如果不是这两个参数将会无法登录
4.1.表单配置中实现自定义用户名和密码参数
4.2. 在过滤器中设置参数
这个可以在UsernamePasswordAuthenticationFilter中看到
如果想要实现自定义参数可以继承此接口,将自定义过滤器加到security过滤链中即可。