Spring Security自定义登录验证及登录返回结果
- 一.功能描述
- 二.处理逻辑
- 简单流程
- 自定义UserDetails
- 自定义UserDetailsDAO
- 自定义UserDetailsService
- 自定义用户登录验证逻辑AuthenticationProvider
- 三.Spring Security基本配置信息
- 四.登录登出自定义处理器
- 登录成功处理器LocalAuthenticationSuccessHandler
- 登录失败处理器LocalAuthenticationFailureHandler
- 登出成功处理器LocalLogoutSuccessHandler
- 匿名用户访问无权限处理器LocalAuthenticationEntryPoint
- 认证用户访问无权限处理器LocalAccessDeniedHandler
- 旧用户被踢处理器LocalSessionInformationExpiredStrategy
一.功能描述
使用Spring Boot + Spring Security实现自定义登录验证及登录登出返回结果.
适用于前后端分离开发的项目.
如有不妥之处, 请各位大神指教.
二.处理逻辑
简单流程
验证成功
验证失败
Request登录
Spring Security
自定义LocalAuthenticationProvider
自定义LocalAuthenticationSuccessHandler
自定义LocalAuthenticationFailureHandler
验证成功
Request登出
Spring Security
自定义LocalLogoutSuccessHandler
自定义UserDetails
@javax.persistence.Entity
@org.hibernate.annotations.Table(appliesTo = "local_user_details")
public class LocalUserDetails implements java.io.Serializable, UserDetails {
private static final long serialVersionUID = 1594783117560L;
@javax.persistence.Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "[id]", nullable = false, unique = true, length = 0, precision = 20, scale = 0, columnDefinition = "bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键'")
@ApiModelProperty(value = "主键", required = false, hidden = false, example = "主键")
@JsonInclude(value = JsonInclude.Include.NON_EMPTY)
private Long id;
@NonNull
@Size(min = 3, max = 18)
@Column(name = "[userid]", nullable = false, unique = false, length = 64, precision = 0, scale = 0, columnDefinition = "varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '用户索引'")
@ApiModelProperty(value = "用户索引", required = true, hidden = false, example = "用户索引")
@JsonInclude(value = JsonInclude.Include.NON_EMPTY)
private String userid = "";
@NonNull
@Size(min = 3, max = 64)
@Column(name = "[passwd]", nullable = false, unique = false, length = 128, precision = 0, scale = 0, columnDefinition = "varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT '用户密码'")
@ApiModelProperty(value = "用户密码", required = true, hidden = false, example = "用户密码")
@JsonInclude(value = JsonInclude.Include.NON_EMPTY)
private String passwd = "";
public LocalUserDetails() {}
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserid() {
return this.userid;
}
public void setUserid(String userid) {
this.userid = userid;
}
public String getPasswd() {
return this.passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return new ArrayList<>();
}
@Override
public String getPassword() {
return this.getPasswd();
}
@Override
public String getUsername() {
return this.getUserid();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
自定义UserDetailsDAO
public interface LocalUserDetailsDAO extends JpaRepository<LocalUserDetails, Long>, JpaSpecificationExecutor<LocalUserDetails> {
Optional<LocalUserDetails> findByUserid(String userid);
}
自定义UserDetailsService
@Service
@Transactional
public class AuthorityUserLoginInfoService implements UserDetailsService {
@Autowired
private LocalUserDetailsDAO dao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<LocalUserDetails> user = dao.findByUserid(username);
if (user.isPresent()) {
if (!user.get().isEnabled()) { throw new DisabledException("暂无权限!"); }
} else {
throw new UsernameNotFoundException("用户名或密码不正确!");
}
return user.get();
}
}
自定义用户登录验证逻辑AuthenticationProvider
@Component
public class LocalAuthenticationProvider implements AuthenticationProvider {
@Autowired
private AuthorityUserLoginInfoService userService;
private final PasswordEncoder encoder = new BCryptPasswordEncoder();
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = (String) authentication.getCredentials();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session = attributes.getRequest().getSession();
Object others = session.getAttribute("others");
UserDetails node = userService.loadUserByUsername(username);
if (!encoder.matches(password, node.getPassword())) { throw new BadCredentialsException("用户名或密码不正确!"); }
return new UsernamePasswordAuthenticationToken(node, password, node.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return true;
}
}
三.Spring Security基本配置信息
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Value(value = "${ignoringAntMatchers:/**}")
private String ignoringAntMatchers;
@Value(value = "${loginPage:/login}")
private String loginPage;
@Value(value = "${loginProcessingUrl:/api/user/login}")
private String loginProcessingUrl;
@Value(value = "${logoutUrl:/api/user/logout}")
private String logoutUrl;
@Value(value = "${expiredUrl:/login}")
private String expiredUrl;
@Autowired
private DataSource dataSource;
@Autowired
private AuthorityUserLoginInfoService userService;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(4);
}
@Bean
public AuthenticationProvider authenticationProvider() {
return new LocalAuthenticationProvider();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
// 指定密码加密所使用的加密器为passwordEncoder(), 需要将密码加密后写入数据库
auth.userDetailsService(this.userService).passwordEncoder(passwordEncoder());
// 不删除凭据,以便记住用户
auth.eraseCredentials(false);
}
@Override
public void configure(WebSecurity web) throws Exception {
// 设置不拦截规则
web.ignoring().antMatchers(this.ignoringAntMatchers.split(","));
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 设置拦截规则
http.authorizeRequests().anyRequest().authenticated();
// 自定义登录信息
http.csrf().disable().formLogin() //
.loginPage(this.loginPage) // 自定义登录页url,默认为/login
.loginProcessingUrl(this.loginProcessingUrl) // 登录验证地址, 即RequestMapping地址
// 用户名的请求字段. 默认为org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY
.usernameParameter("username")
// 用户名的请求字段. 默认为org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY
.passwordParameter("password") //
.successHandler(new LocalAuthenticationSuccessHandler()) // 登录验证成功后, 执行的内容
// .defaultSuccessUrl("/index") // 登录验证成功后, 跳转的页面, 如果自定义返回内容, 请使用successHandler方法
// .successForwardUrl("/index") // 登录验证成功后, 跳转的页面, 如果自定义返回内容, 请使用successHandler方法
.failureHandler(new LocalAuthenticationFailureHandler()) // 登录验证失败后, 执行的内容
// .failureUrl("/login?error") // 登录验证失败后, 跳转的页面, 如果自定义返回内容, 请使用failureHandler方法
.permitAll();
// 自定义异常处理器
http.exceptionHandling() // 异常处理器
// 用来解决匿名用户访问无权限资源时的异常
.authenticationEntryPoint(new LocalAuthenticationEntryPoint())
// 用来解决认证过的用户访问无权限资源时的异常, 跳转的页面, 如果自定义返回内容, 请使用accessDeniedHandler方法
// .accessDeniedPage("/error")
// 用来解决认证过的用户访问无权限资源时的异常
.accessDeniedHandler(new LocalAccessDeniedHandler());
// 自定义注销信息
http.logout() //
.logoutUrl(this.logoutUrl) // 登出验证地址, 即RequestMapping地址
.logoutSuccessHandler(new LocalLogoutSuccessHandler()) // 登录验证成功后, 执行的内容
// .logoutSuccessUrl("/login?logout") // 登录验证成功后, 跳转的页面, 如果自定义返回内容, 请使用logoutSuccessHandler方法
.deleteCookies("JSESSIONID") // 退出登录后需要删除的cookies名称
.invalidateHttpSession(true) // 退出登录后, 会话失效
.permitAll();
// remember-me配置
http.rememberMe() //
// org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer.DEFAULT_REMEMBER_ME_NAME
.rememberMeCookieName("remember-me")
// org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer.DEFAULT_REMEMBER_ME_NAME
.rememberMeParameter("remember-me")
// org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices.TWO_WEEKS_S
.tokenValiditySeconds(360000)
// tokenRepository
.tokenRepository(new LocalPersistentTokenRepository(this.dataSource));
// session管理
http.sessionManagement().sessionFixation().changeSessionId() //
.maximumSessions(1) // 最大会话数
.maxSessionsPreventsLogin(false) // 达到最大数后, 强制验证, 踢出旧用户
// 旧用户被踢出后, 跳转的页面, 如果自定义返回内容, 请使用expiredSessionStrategy方法
// .expiredUrl("/login?expired")
// 旧用户被踢出后, 执行的内容
.expiredSessionStrategy(new LocalSessionInformationExpiredStrategy());
}
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
四.登录登出自定义处理器
登录成功处理器LocalAuthenticationSuccessHandler
public class LocalAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private static final Gson gson = new GsonBuilder().disableHtmlEscaping().create();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(gson.toJson(new Result<>(ResultStatus.SUCCESS_LOGIN)));
}
}
登录失败处理器LocalAuthenticationFailureHandler
public class LocalAuthenticationFailureHandler implements AuthenticationFailureHandler {
private static final Gson gson = new GsonBuilder().disableHtmlEscaping().create();
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(gson.toJson(new Result<>(ResultStatus.ERROR_LOGIN)));
}
}
登出成功处理器LocalLogoutSuccessHandler
public class LocalLogoutSuccessHandler implements LogoutSuccessHandler {
private static final Gson gson = new GsonBuilder().disableHtmlEscaping().create();
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(gson.toJson(new Result<>(20001, "登出成功!")));
}
}
匿名用户访问无权限处理器LocalAuthenticationEntryPoint
public class LocalAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Gson gson = new GsonBuilder().disableHtmlEscaping().create();
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(gson.toJson(new Result<>(ResultStatus.WRONG_DEAL)));
}
}
认证用户访问无权限处理器LocalAccessDeniedHandler
public class LocalAccessDeniedHandler implements AccessDeniedHandler {
private static final Gson gson = new GsonBuilder().disableHtmlEscaping().create();
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(gson.toJson(new Result<>(ResultStatus.WRONG_DEAL)));
}
}
旧用户被踢处理器LocalSessionInformationExpiredStrategy
public class LocalSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
private static final Gson gson = new GsonBuilder().disableHtmlEscaping().create();
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
HttpServletResponse response = event.getResponse();
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(gson.toJson(new Result<>(20002, "用户已退出!")));
}
}