共计 8491 个字符,预计需要花费 22 分钟才能阅读完成。
本文思维导图
图 1 思维导图
原理探讨
当咱们在我的项目中引入 Spring Security 的相干依赖后,默认的就是表单登录模式;俗话说:“听人劝,吃饱饭”,既然 Spring Security 曾经给咱们安顿的明明白白了,咱们就从表单登录开始吧。
在开始之前,咱们能够站在 Spring Security 的角度上思考:如果我本人来实现表单登录的性能,那么我须要做哪些工作呢?
就我集体而言,我可能会思考以下几点:
- 配置用户信息,存储如账号、明码等;明码不能以明文传输,须要加密性能
- 执行校验
- 认证胜利或者失败的解决计划
能够简略的制作成如下流程图:
图 1 -1 表单登录简略流程图
上方属于咱们本人构想的实现计划,属于 ” 低配版 ” 模式,上面咱们来看看 Spring Security 是怎么做的。Spring Security 的思路和咱们大同小异,长处在于其提供了很好的封装,进步了框架自身的可扩展性。
Spring Security 的实现步骤如下:
- UsernamePasswordAuthenticationFilter 拦截器拦挡前端传递的表单登录申请,将登录信息(username、password)封装成 UsernamePasswordAuthenticationToken,传递给 AuthenticationManager 认证管理器
- AuthenticationManager 认证管理器依据 Token 的类型遍历获取对应的 Provider,也即是 DaoAuthenticationProvider,执行认证流程
- DaoAuthenticationProvider 依附 PasswordEncoder 和 UserDetailsService 对登录申请进行验证
- 验证通过,由 AuthenticationSuccessHandler 认证胜利处理器进行解决
- 验证失败,由 AuthenticationFailureHandler 认证失败处理器进行解决
制作成流程图如示:
图 1 -2 Spring Security 表单登录认证流程图
这时你可能会一脸懵逼:这咋和刚刚咱们本人构想的齐全不一样呀~ 又是 Manager 又是 Provider 的;莫慌,且听我缓缓道来。
下面呈现了很多新的概念,咱们目前不须要非常粗疏的理解它们是怎么发挥作用的,只须要大略晓得它们有什么用的即可;具体的介绍会在下篇《认证(二):表单登录认证流程源码解析》娓娓道来。
- UsernamePasswordAuthenticationFilter 表单登录拦截器,用以捕捉前端传递的登录信息(username、password),并将登录信息封装成某些 Token。
- AuthenticationManager 认证管理器,可简略的了解为调配工作的领导。DaoAuthenticationProvider DAO 认证处理器,相当于被安顿干活的童鞋;从名字 DAO 也能够简略的揣测出:它与数据库中的用户信息密不可分。
- PasswordEncoder 明码加密器,明码不能明文传输,须要加密。UserDetailsService 用户信息 Service 层,这个也很好了解,前端传递的登录信息必定是有对应的数据库实体存储。
- AuthenticationSuccessHandler 认证胜利处理器 AuthenticationFailureHandler 认证失败处理器。
通过上述的原理探讨,咱们大体上能弄懂了整个表单登录有哪几个模块须要解决;可简略的总结为 3 个模块:
- 登录前置解决:用户信息的封装、明码加密器的设置
- 登录中解决:登录的校验
- 登录后置解决:登录失败、登录胜利的解决计划
小试牛刀
俗话说:“光说不练假把式”,那么就让咱们来实战一番吧。
登录前置解决
作为一个 Java Web 我的项目,第一步当然是引入相干依赖;间接引入 Spring Boot 封装好的 starter 即可。
Step-1 配置用户信息
Spring Security 提供了 UserDetails 接口,用于获取用户的根本信息(账号密码、权限汇合、是否锁定等等),咱们只须要依据本身的业务场景,实现该接口即可。
Spring Security 提供的 UserDetails.class 接口
自定义业务相干的用户信息类,业务定义的 UserInfo.class 必须带有 username 和 password 相干的信息,用于做用户验证;我的项目依据本身需要来判断是否须要应用上面的几个 boolean 办法,如果无相干需要则间接返回 true 即可。
@Setter
public class UserInfo implements UserDetails {
private String username;
private String password;
/**
* UserDetails 的接口
* 用户权限集,默认须要增加 ROLE_作为前缀
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>(1);
simpleGrantedAuthorities.add(new SimpleGrantedAuthority(“ROLE_USER”));
return simpleGrantedAuthorities;
}
/**
* 获取用户明码
*/
@Override
public String getPassword() {
return this.password;
}
/**
* 获取用户名
*/
@Override
public String getUsername() {
return this.username;
}
/**
* 账户是否未过期 –true 则为未过期
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 账户是否未被锁定
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 账户凭证是否未过期
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 账户是否可用
*/
@Override
public boolean isEnabled() {
return true;
}
}
在定义完用户实体 UserInfo 后,咱们同时也须要提供对应的 Service 层的 API 办法,用以进行一些根本的操作,诸如:新增用户、删除用户等。
Spring Security 也提供了对应的 Service 层接口,UserDetailsService,接口只有一个办法:UserDetails loadUserByUsername(String username);依据用户名加载用户信息.
UserDetailsService.class
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
因而咱们能够自定义业务相干的 UserInfoServiceImpl 类,实现 Spring Security 提供的 UserDetailsService 接口
UserInfoServiceImpl.class
/**
* 用户信息 service 模块
*
* UserDetailsService 接口为 SpringSecurity 内置接口,外部有办法:
* UserDetails loadUserByUsername(String username): 如名所得 依据用户名加载用户
* 该办法次要是在:DaoAuthenticationProvider 中被调用,获取用户的信息
*
* @author 小奇
*/
@Slf4j
@Service
public class UserInfoServiceImpl implements UserDetailsService, UserInfoService {
private final UserInfoDAO userInfoDAO;
@Autowired
public UserInfoServiceImpl(UserInfoDAO userInfoDAO) {
this.userInfoDAO = userInfoDAO;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<UserInfo> userInfoOpt = Optional.ofNullable(userInfoDAO.loadUserByUsername(username));
UserInfo user = userInfoOpt.orElseThrow(() -> new UsernameNotFoundException(“can’t not load user by username”));
log.info(“ 依据用户名:{} 查问用户胜利 ”, user.getUsername());
return user;
}
}
Step-2 配置明码加密器
家喻户晓,明码是不能以明文的形式存储的,贴心的 Spring Security 天然不会遗记提供加密的性能。PasswordEncoder 接口,次要提供 2 个办法;String encode(CharSequence rawPassword)办法用于加密,由咱们在注册用户的时候调用;boolean matches(CharSequence rawPassword, String encodedPassword) 办法用于匹配,登录验证时由 Spring Security 框架调用。
PasswordEncoder.class
如果我的项目有本人的加解密形式,只须要实现该接口即可,如果没有能够尝试应用 Spring 提供的 BCryptPasswordEncoder 明码加密器。
登录中解决
在这一块上,咱们能够自定义与本身业务无关的登录逻辑判断,目前没有这种需要就应用 Spring Security 提供的默认实现即可。
登录后置解决
登录的后置解决分两种状况,第一种是登录胜利的解决,一种是登录失败的解决。
Step-03 配置登录胜利处理器
Spring Security 提供了认证胜利处理器接口 AuthenticationSuccessHandler,当咱们有一些自定义的业务逻辑,诸如:用户登录胜利后赠送积分,或者登录胜利后主动跳转……就能够通过提供该接口的自定义实现。
AuthenticationSuccessHandler.class
public interface AuthenticationSuccessHandler {
/**
* 默认办法
*/
default void onAuthenticationSuccess(HttpServletRequest request,HttpServletResponse response, FilterChain chain, Authentication authentication)
throws IOException, ServletException{
onAuthenticationSuccess(request, response, authentication);
chain.doFilter(request, response);
}
/**
* 胜利后会被调用
*/
void onAuthenticationSuccess(HttpServletRequest request,HttpServletResponse response, Authentication authentication)
throws IOException, ServletException;
}
自定义胜利处理器 WebAuthenticationSuccessHandler.class
/**
* 自定义验证胜利处理器
* @author 小奇
*/
@Slf4j
@Component
public class WebAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
log.info(“ 登录胜利~~”);
// 返回 json 可增加本身业务逻辑 如:登录胜利后增加用户积分等……
response.setContentType(“application/json;charset=UTF-8”);
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
Step-04 配置登录失败处理器
AuthenticationFailureHandler 失败处理器和胜利处理器相似,不做过多的解析,上代码。
public interface AuthenticationFailureHandler {
/**
* 失败后调用
*/
void onAuthenticationFailure(HttpServletRequest request,HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException;
}
自定义失败处理器 WebAuthenticationFailureHandler.class
/**
* 自定义验证失败处理器
* @author 小奇
*/
@Slf4j
@Component
public class WebAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
log.error(“ 登录失败 ”);
// 把 exception 返回给前台
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType(“application/json;charset=UTF-8”);
response.getWriter().write(objectMapper.writeValueAsString(exception));
// 可做其余业务逻辑,诸如限度每天登录失败的次数
}
}
Step-05 配置 SecurityConfig
还记得之前咱们提过的 Spring Security 为人广为诟病的繁琐配置吗?自从搭上 Spring Boot 的列车之后,有了天翻地覆的扭转。
上面就来简略配置一下咱们在下面自定义的一些模块吧。
/**
* @author kylin
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private WebAuthenticationSuccessHandler successHandler;
@Autowired
private WebAuthenticationFailureHandler failureHandler;
/**
* 明码加密器,应用 spring 提供的 BCryptPasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* http 申请平安配置
*
* @param http
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(“/resources/“, “/css/“, “/about”, “/test”).permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage(“/login.html”)
.successHandler(successHandler)
.failureHandler(failureHandler)
.permitAll()
.and()
.csrf().disable();
}
}
整个配置就根本实现了,也比较简单易懂;对一些配置进行根底的解说
- .antMatchers(“/resources/“, “/css/“, “/about”, “/test”).permitAll()是指对于这些正则门路进行放行
- loginPage(“/login.html”)指的是自定义了一个前端的登录页面,当然也能够应用默认的页面(只是绝对比拟简陋了些)
- 最初的 csrf 记得敞开,这一块前面会专门介绍。
总结
以上内容则为本文的全内容,文章通过原理探讨、入手尝试逐个开展。如有谬误之处,请多多斧正