Spring Security应用(一)

  用了Spring,自然要用研究和使用Spring Security.

  Spring Security 一句话概括:一组 filter 过滤链组成的权限验证。

一、基本原理

  Spring Security的整个工作流程如图:

  

  绿色认证方式可以配置, 橘黄色和蓝色的位置不可更改。

  Security 两种认证方式:

    1. httpbasic

    2. formLogin 默认的,不进行任何配置的方式

  同样,Security 也提供两种过滤器类:

    1. UsernamePasswordAuthenticationFilter 表示表单登陆过滤器

    2.BasicAuthenticationFilter 表示 httpbaic 方式登陆过滤器

  图中橙色的FilterSecurityInterceptor 是最终的过滤器,它会决定当前的请求可不可以访问Controller,判断规则放在这个里面。当不通过时会把异常抛给在这个过滤器的前面的 ExceptionTranslationFilter 过滤器。

  ExceptionTranslationFilter 接收到异常信息时,将跳转页面引导用户进行认证,如上方所示的用户登陆界面。

二、自定义认证逻辑

  实际开发是不可能使用 Spring Security 默认的认证方式的,如何去覆盖 Spring Security 默认的配置呢?

  创建SpringSecurity自定义配置类:WebSecurityConfig.java

  

  重新启动项目,已经看到修改后的 httpbasic 方式认证了

  

  在这里我们依然采用的默认提供的用户名 user,以及每次服务器启动自动生成的 password,那么可不可以自定义认证逻辑呢?比如采用数据库中的用户登陆?

  答案是肯定的。

  自定义用户认证逻辑需要了解三步:

    1.  处理用户信息获取逻辑

    2. 处理用户效验逻辑

    3. 处理密码加密解密

  我们来看下这三步,然后实现自定义登录。

2.1 处理用户信息获取逻辑

  Spring Security 中用户信息获取逻辑是封装在一个接口里:UserDetailService

  

  这个接口中只有一个方法,loadUserByUsername(), 该接收一个 String 类型的 username 参数,然后返回一个 UserDetails 的对象。

loadUserByUsername() 这个方法到底干什么的?

  通过前台用户输入的用户名,然后去数据库存储中获取对应的用户信息,然后封装在 UserDetail 实现类里面。

  封装到 UserDetail 实现类返回以后,Spring Srcurity 会拿着用户信息去做校验,如果校验通过了,就会把用户放在 session 里面,否则,抛出 UsernameNotFoundException 异常,Spring Security 捕获后做出相应的提示信息。

  想要处理用户信息获取逻辑,那么我们就需要自己去实现 UserDetailsService

 新建 UserDetailsServiceImpl.java

  

 SecurityUserDetail.java

public class SecurityUserDetails extends User implements UserDetails {

    private static final long serialVersionUID = 1L;

    public SecurityUserDetails(User user) {

        if(user!=null) {
            this.setUsername(user.getUsername());
            this.setPassword(user.getPassword());
            this.setStatus(user.getStatus());
        }
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        理想型返回 admin 权限,可自已处理这块
        return AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
    }

    /**
     * 账户是否过期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 是否禁用
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 密码是否过期
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 是否启用
     * @return
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

  至此,处理用户信息获取逻辑 部分完成了,主要实现 UserDetailsService 接口的 loadUserByname 方法。

  为何会用到 SecurityUserDetail 类进行转换一下?

  其实完全可以直接返回一个 User 对象,但是需要注意的是,如果直接返回 User 对象的话,返回的是 security 包下的 user。

  至于为何这样处理,如果返回的是 security 包下的 user,这样就失去了使用本地数据库的意义,下方自定义登陆逻辑详细说明。

2.2 处理用户校验逻辑

  关于用户的校验逻辑主要包含两方面:

   1. 密码是否匹配【由Sprin Security处理,只需要告诉其密码即可】

   2. 密码是否过期、或者账户是否被冻结等

  前者,已经通过实现 UserDetailsService 的 loadUserByname() 方法实现了,接下来主要看看后者。

  用户密码是否过期、是否被冻结等等需要实现 UserDetails 接口:

  

  主要看后四个方法:

    1、isAccountNonExpired() 账户没有过期 返回true 表示没有过期
    2、isAccountNonLocked() 账户没有锁定
    3、isCredentialsNonExpired() 密码是否过期
    4、isEnabled() 是否被删除

2.3 处理密码加密解密

到 WebSecurityConfig 自定义配置类。加入:

配置了这个 configure 方法以后,从前端传递过来的密码就会被加密,所以从数据库查询到的密码必须是经过加密的,而这个过程都是在用户注册的时候进行加密的。

补充:UserDetailsServiceImpl 为自定义的 UserDetailsService 实现类。

三、 自定义认证流程

  同样的在实际的开发中,对于用户的登录认证,不可能使用 Spring Security 自带的方式或者页面,需要自己定制适用于项目的登录流程。

  Spring Security 支持用户在配置文件中配置自己的登录页面,如果用户配置了,则采用用户自己的页面,否则采用模块内置的登录页面。

  WebSecurityConfig 配置类中增加 成功、失败过滤器。

@Autowired
private AuthenticationSuccessHandler successHandler;

@Autowired
private AuthenticationFailHandler failHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {

    ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
            .authorizeRequests();

    registry.and()
        //表单登录方式
        .formLogin()
        .permitAll()
          //成功处理类
        .successHandler(successHandler)
        //失败
        .failureHandler(failHandler)
        .and()
        .logout()
        .permitAll()
        .and()
        .authorizeRequests()
       // 任何请求
        .anyRequest()
        //需要身份认证
        .authenticated()
        .and()
        //关闭跨站请求防护
        .csrf().disable()
        //前后端分离采用JWT 不需要session
     .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}

在添加 AuthenticationSuccessHandler、AuthenticationFailHandler 后会帮我们自动导包,但是,既然是个性化认证流程,自然要我们自己去实现~

那我们究竟要实现什么效果呢?

 

为何要采用这种返回方式?

  用户登录成功后,Spring Security 的默认处理方式是跳转到原来的链接上,这也是企业级开发的常见方式,但是有时候采用的是 Ajax 方式发送的请求,往往需要返回 Json 数据,如图中:登陆成功后,会把 token 返回给前台,失败时则返回失败信息。

AuthenticationSuccessHandler:

Slf4j
@Component
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        String username = ((UserDetails)authentication.getPrincipal()).getUsername();
        List<GrantedAuthority> authorities = (List<GrantedAuthority>) ((UserDetails)authentication.getPrincipal()).getAuthorities();
        List<String> list = new ArrayList<>();
        for(GrantedAuthority g : authorities){
            list.add(g.getAuthority());
        }
        登陆成功生成token
        String  token = UUID.randomUUID().toString().replace("-", "");
    token 需要保存至服务器一份,实现方式:redis or jwt
        输出到浏览器
        ResponseUtil.out(response, ResponseUtil.resultMap(true,200,"登录成功", token));
    }
}

  SavedRequestAwareAuthenticationSuccessHandle r是 Spring Security 默认的成功处理器,默认方式是跳转。

  这里将认证信息作为 Json 数据进行了返回,也可以返回其他数据,这个是根据业务需求来定的,比如,上方代码在用户登陆成功后返回来 token,需要注意的是,此 token 需要在服务器备份一份,毕竟要用做下次的身份认证嘛~

AuthenticationFailHandler:

@Component
public class AuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {

        ## 默认情况下,不管你是用户名不存在,密码错误,SS 都会报出 Bad credentials 异常信息
        if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
            ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"用户名或密码错误"));
        } else if (e instanceof DisabledException) {
            ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"账户被禁用,请联系管理员"));
        } else {
            ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"登录失败,其他内部错误"));
        }
    }

}

猜你喜欢

转载自www.cnblogs.com/FondWang/p/12367089.html