最近在学习springboot 及微服务框架时,在搭建项目的时候想实现登录注册的功能,在网上翻翻找找,感觉还是现在目前比较流行的Spring Security比较靠谱,所以自己尝试搭建了下。spring Security的特性,如果感兴趣可以查一查,毕竟我不是它的推广者么,只是使用者,如果你感觉它还可以,那就有必要到这里来看下了,这里只讲一下其简单的工作原理和搭建流程。
说明
在开始之前,个人首先说明下,目前使用spring Security 目前主要的2个场景, 一个是单个服务的使用session的场景,另一个是使用JWT验证的 微服务模式。在这里本人做的是基于session的验证,但是在后面会留出 JWT验证的扩展。
源码和原理----简单分析
说到Security, 我们首先会想到就是 用户的登录验证。所以先从验证这一点慢慢来进行剖析其原理。
AuthenticationManager
首先,需要讲明的是AuthenticationManager,其是认证管理器,通过源码可知他是一个接口,只对认证做了一个声明验证的方法。具体的验证方法,都交由Provider 来实现。
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationProvider
AuthenticationProvider 也是一个接口,他是最基本的接口,也定义了验证的方法,所以我们具体实现的时候只要定义一个自己的类,实现这个接口,复写一下认证方法就行了。
package org.springframework.security.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}
PS: 有人可能问,AuthenticationManager 和 AuthenticationProvider 都有认证,而且都是接口,怎么交付给Provider来验证的?源码中个人没看到,但是我试着猜想下,可能是Manager的实现类中验证方法调用了provider的 啊!
UserDetails
好吗,其实当个人创建实现AuthenticationProvider类,进行验证,其实到这就完成基本功能的一半多了。现在想一下,自己的实现类如何验证?肯定是从前段获取的数据 与 数据库存储的信息进行对比么。现在基本的开发框架中 持久层中,基本上都是ORM的了吧。 Security其实也为我们提供了一个用户的基本的接口 UserDetails。其对特定的方法做了定义:
package org.springframework.security.core.userdetails;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import java.io.Serializable;
import java.util.Collection;
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getUsername();
//下面的几个参数,怎么用自己设计
//账号是否过期
boolean isAccountNonExpired();
//账号是否被锁
boolean isAccountNonLocked();
//身份是否过期
boolean isCredentialsNonExpired();
//是否可用
boolean isEnabled();
}
UserDetailsService
有了实体类了,那就需要操纵这个实体类的 service 层了,UserDetailsService这个接口就是对默认接口的定义,目前 只是定义了一个函数,这个函数是其验证默认调用的,自己验证的时候可以用别的函数,但是 为了原生态点还是不要动了。
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
这就相当于 业务逻辑层,当然mapper 数据持久层害的自己去实现,无非是用户的增删改查功能。这些全都弄完了,就差一个配置问题了,你不把 Provider 配置到Security中去,他还是会默认自己原来的配置。
这个配置的基本接口是 WebSecurityConfigurer,但是现在已经有WebSecurityConfigurerAdapter抽象类对其 相应的方法进行了实现和扩展,所以我们只需要集成这个类,重写一下 配置方法就可以了。
到现在基本的验证 基本就可以了。下面实现的时候再加入handler的和filter的。
流程
现在抛出上面的想法,我们以一个完整的流程说一下工作流程。
- 前段输入用户名密码,点击登录按钮,跳转到security配置中配置的登录验证url (这个url可以配置)
- 相应的filter 对 请求进行拦截处理(这个filter 也可以自己定义,使用JWT时需要自己实现)
- 交付AuthenticationManager,并交由provider进行验证(验证过程中,需要我们实现的实体类,service层和mapper 持久层,这些都是我们自己定义实现的)。
- 将验证结果返回相应的filter
- filter在调用处理相应 状况的handler 进行最后处理(这个也可以自己去实现)
实现
- 添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>2.0.0.RELEASE</version> </dependency>
添加完依赖后,启动项目访问url后会出现一个登录的页面,用户名是 user, 密码在后台log中打印出来
- 配置springSecurity 的配置
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * @Authortao * @Description 覆写父类的request 配置,主要是对请求路径和资源的限制 * @Param * @return * @Date 19:34 2018-12-27 **/ @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/register").permitAll() //允许所有人 调用注册请求 .anyRequest().authenticated() //其他请求必须要验证 .and() //设置免登陆的页面和资源 .formLogin() .loginProcessingUrl("/login/submit") //设置登录验证的url .permitAll() .and() .csrf().disable();//关闭csrf验证 super.configure(http); }
- 添加自己创建的provider 并实现认证方法
/** * @ClassName UserAuthenticationProvider * @Descriptiom 我们自定义的provider 对象 * @Author * @Date 2018-12-27 20:37 * @Version 1.0v **/ @Component public class UserAuthenticationProvider implements AuthenticationProvider { @Resource private UserService userDetailService; BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); //开始验证 @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { System.out.println("provider 验证开始"); //获取用户名和密码 String username = authentication.getName(); String password = (String) authentication.getCredentials(); Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); //查看是否含有该用户信息 User user = (User) userDetailService.loadUserByUsername(username); //如果没有 if (user == null){ throw new BadCredentialsException("用户不存在!!"); } if( ! encoder.matches(password,user.getPassword())){ throw new BadCredentialsException("密码不正确!!"); } return new UsernamePasswordAuthenticationToken(username,password,authorities); } @Override public boolean supports(Class<?> authentication) { return true; } }
修改 配置文件,并添加一下代码,重启项目
@Resource UserAuthenticationProvider provider; /** * @Author * @Description 覆盖父类的关于 验证的配置 * @Param * @return * @Date 19:34 2018-12-27 **/ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //配置验证的provider auth.authenticationProvider(provider); }
打个断点启动项目,你会发现配置么,就是在启动的时候编译的。
- 美化验证结果输出,实现相应的Handler
@Component("userAuthenticationSuccessHandler") public class UserAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { System.out.println("这是provider的成功验证"); // super.onAuthenticationSuccess(request, response, authentication); Map<String,String> map=new HashMap<>(); map.put("code", "2000"); map.put("msg", "登录成功"); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(map)); //如果是要跳转到某个页面的 new DefaultRedirectStrategy().sendRedirect(request, response, "/dudu/sss"); } }
@Component("userAuthenticationFailHander") public class UserAuthenticationFailHander extends SimpleUrlAuthenticationFailureHandler { @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { System.out.println("这是Hander的失败验证"); // super.onAuthenticationFailure(request, response, exception); Map<String,String> map=new HashMap<>(); map.put("code", "2001"); map.put("msg", "登录失败"); response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(map)); } }
并修改 配置类为
protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/register").permitAll() .anyRequest().authenticated() .and() //设置免登陆的页面和资源 .formLogin() .loginProcessingUrl("/login/submit") .failureHandler(userAuthenticationFailHander) .successHandler(userAuthenticationSuccessHandler) .permitAll() .and() .addFilter(new UserAuthenticationFilter(authenticationManager(),userAuthenticationFailHander,userAuthenticationSuccessHandler)) super.configure(http); }
- 倘若想 实现Filter
public class UserAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private AuthenticationManager authenticationManager; UserAuthenticationFailHander userAuthenticationFailHander; @Autowired UserAuthenticationSuccessHandler userAuthenticationSuccessHandler; private RememberMeServices rememberMeServices = new NullRememberMeServices(); public UserAuthenticationFilter(AuthenticationManager authenticationManager, UserAuthenticationFailHander userAuthenticationFailHander, UserAuthenticationSuccessHandler userAuthenticationSuccessHandler) { //这个构造方法实在启动、编译的时候调用, this.authenticationManager = authenticationManager; this.userAuthenticationFailHander = userAuthenticationFailHander; this. userAuthenticationSuccessHandler = userAuthenticationSuccessHandler; //添加拦截的路径 super.setFilterProcessesUrl("/login/submit"); } //当相应的路径进行请求的时候,进行向相应的处理 @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { System.out.println("我们要执行filter 了哈"); //这个方法说实话现在不能用,因为这个是form 提交啊, 没有指定 相应元素的 name 元素 // User user = new ObjectMapper().readValue(request.getInputStream(), User.class); String username = obtainUsername(request); String password = obtainPassword(request); { //这里可以做一下登录的验证啊,比如: 用户名、密码是否为空,验证码的正确与否 } //倘若上述的验证全都通过的话,我们去验证用户名和密码是否匹配, 下面是存储的权限 Collection<? extends GrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_ADMIN")); //验证的时候,他的基本原理是 AuthenticationManager进行验证,但是其是一个接口,具体的实现在相应的provider中 return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(username, password, authorities)); // return super.attemptAuthentication(request, response); } @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { System.out.println("大家注意了哈!!!!!,这是 filter的 成功验证调用,successfulAuthentication-------------------------------------->>>>>>>>>>>>>>>>>>"); SecurityContextHolder.getContext().setAuthentication(authResult); userAuthenticationSuccessHandler.onAuthenticationSuccess(request, response, authResult); // rememberMeServices.loginSuccess(request, response, authResult); // super.successfulAuthentication(request, response, chain, authResult); } @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { System.out.println("大家注意了哈!!!!!,这是 filter的 失败验证调用,unsuccessfulAuthentication****************************************************************"); SecurityContextHolder.clearContext(); if (logger.isDebugEnabled()) { logger.debug("Authentication request failed: " + failed.toString(), failed); logger.debug("Updated SecurityContextHolder to contain null Authentication"); logger.debug("Delegating to authentication failure handler " + userAuthenticationFailHander); } // rememberMeServices.loginFail(request, response); userAuthenticationFailHander.onAuthenticationFailure(request, response, failed); // super.unsuccessfulAuthentication(request, response, failed); } }
这里需要再次修改配置类
@Autowired UserAuthenticationFailHander userAuthenticationFailHander; @Autowired UserAuthenticationSuccessHandler userAuthenticationSuccessHandler; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/register").permitAll() .anyRequest().authenticated() .and() //设置免登陆的页面和资源 .formLogin() .loginProcessingUrl("/login/submit") // .failureHandler(userAuthenticationFailHander) // .successHandler(userAuthenticationSuccessHandler) .permitAll() .and() .addFilter(new UserAuthenticationFilter(authenticationManager(),userAuthenticationFailHander,userAuthenticationSuccessHandler)) // // 不需要session // .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // .and() .csrf().disable(); super.configure(http); }
-
如果想使用JWT,则需要在filter 验证成功后,创建一个token,并存入 Redis中, 并将token塞入header 中返回给前段,让前段以此登录认证。另外还需要将上面 session 关闭,这时候就不要session 存储登录信息了。