之前都是自个实现登录拦截、用户验证,自从发现了研究springSecurity这个框架之后,爱不释手,基本上权限+验证方面的操作都可以完成,是spring中一个重量级的安全校验框架。

       很多人抱怨springSecurity的api太复杂,其实不然,当你了解它的流程操作时,用起来相当顺手,搭配springboot可快速搭建一个有权限控制级别的项目。

      在使用springSecurity之前,我们先来认识一下它的核心类。

Authentication

      Authentication是表示用户信息的一个接口,通过它可封装一些用户登录认证的对象,用户认证成功后,又会生成一个信息更加全面,包括用户所拥有的角色等信息的Authentication对象,然后把它保存在SecurityContextHolder 所持有的 SecurityContext 中,供后续的程序进行调用,如访问权限的鉴定等。

SecurityContextHolder

      根据以上所知,它是一个用来保存SecurityContext的一个类,而SecurityContext中又存储着用户信息。它含有很多静态方法,如getContext、setContext、clearContext来对SecurityContext进行操作。

AuthenticationManager 和 AuthenticationProvider

      AuthenticationManager 是一个用来处理认证(Authentication)请求的接口。在其中只定义了一个方法 authenticate(),该方法只接收一个代表认证请求的 Authentication 对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的 Authentication 对象进行返回。

     AuthenticationManager 的默认实现是 ProviderManager,它不自己处理请求,而是通过委托机制给其配置的AuthenticationProvider 列表,然后会依次使用每一个 AuthenticationProvider 进行认证,如果有一个 AuthenticationProvider 认证后的结果不为 null,则表示该 AuthenticationProvider 已经认证成功,之后的 AuthenticationProvider 将不再继续认证

UserDetailsService

        认识这个接口之前我们先来看一下UserDetails接口,UserDetails中定义了用户的账户、密码、权限等信息,可通过实现该接口中的方式自行定义用户信息类。而UserDetailsService中只有一个方法loadUserByUsername(),该方法返回的便是一个UserDetails对象。这个方法主要是对web传来的用户信息进行验证操作,验证成功便返回用户所属的UserDetails信息。用户验证不通过则抛出UsernameNotFoundException异常。

 现在我们来开始搭建我们的springSecurity。

首先,定义好我们的用户信息类User

@Entity
@Table(name = "SISE_USER")
public class User extends BaseEntity{
    @Column(name="USER_USERNAME",length=400)
    private String username;
    @Column(name="USER_ACCOUNT",length=400)
    private String account;
    @Column(name="USER_PASSWORD",length = 400)
    private String password;
    @Column(name="USER_EMAIL",length = 400)
    private String email;
    @Column(name = "USER_PHONE",length = 400)
    private String phone;
    @ManyToMany(fetch = FetchType.EAGER, targetEntity = Role.class)
    @JoinTable(name = "TT_USER_ROLE",joinColumns = @JoinColumn(name = "USER_ID"),inverseJoinColumns = @JoinColumn(name = "ROLE_ID"))
    private Set<Role> roles = new LinkedHashSet<>();

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getAccount() {
        return account;
    }

    public void setAccount(String account) {
        this.account = account;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public Set<Role> getRoles() {
        return roles;
    }

    public void setRoles(Set<Role> roles) {
        this.roles = roles;
    }
}

 然后创建一个SecurityUser类来继承这个User和实现Usertails接口

public class SecurityUser extends User implements UserDetails {

    private static final long serialVersionUID = 1L;

    public SecurityUser(User suser) {

        if (suser != null)

        {

            this.setsId(suser.getsId());

            this.setUsername(suser.getUsername());

            this.setAccount(suser.getAccount());

            this.setEmail(suser.getEmail());

            this.setPassword(suser.getPassword());


            this.setRoles(suser.getRoles());

        }

    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
        Set<Role> roles = super.getRoles();
        if(roles != null){
            for(Role role : roles){
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getRoleName());
                authorities.add(authority);
            }
        }
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

 到了这里,我们的用户信息基本定义完成。

接下来我们来实现UserDetailsService接口,我们创建一个CustomerUserService来实现它。

@Component
public class CustomUserDetailsService implements UserDetailsService{

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
        User user = userService.findByAccount(account);
        if(user == null){
            throw new UsernameNotFoundException("Username :" + account + "not found");
        }
        SecurityUser securityUser = new SecurityUser(user);
        return securityUser;
    }
}

记得使用@Component把它注册为组件,便于spring进行实例化。

接下来就是要对springSercurity进行配置和使用,我们创建一个WebSecurityConfig继承WebSecurityConfiguConfigurerAdater

@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.authorizeRequests().anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login_p")
                .loginProcessingUrl("/login")
                .usernameParameter("account")
                .passwordParameter("password")
                .permitAll()
                .failureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                httpServletResponse.setContentType("application/json;charset=utf-8");
                PrintWriter out = httpServletResponse.getWriter();

                StringBuffer sb = new StringBuffer();
                sb.append("{\"status\":\"error\",\"msg\":\"");
                if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
                    sb.append("用户名或密码输入错误,登录失败!");
                } else if (e instanceof DisabledException) {
                    sb.append("账户被禁用,登录失败,请联系管理员!");
                } else {
                    sb.append("登录失败!");
                }
                sb.append("\"}");
                out.write(sb.toString());
                out.flush();
                out.close();
            }
        }).successHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                httpServletResponse.setContentType("application/json;charset=utf-8");
                PrintWriter out = httpServletResponse.getWriter();
                SecurityUser user = (SecurityUser) SecurityContextHolder.getContext()
                        .getAuthentication()
                        .getPrincipal();
                Set<Menu> menus = new HashSet<>();
                for(Role role : user.getRoles()){
                    menus.addAll(role.getMenus());
                }
                ArrayNode datas = JsonUtils.createArrayNode();
                ObjectNode data;
                data = datas.addObject();
                data.put("account",user.getAccount());
                data.put("userId",user.getsId());
                data.put("username",user.getUsername());
                ObjectMapper objectMapper = new ObjectMapper();
                String s = "{\"status\":\"success\",\"msg\":" + objectMapper.writeValueAsString(datas) + "}";
                out.write(s);
                out.flush();
                out.close();
            }
        }).and().logout().permitAll().and().csrf().disable();
    }

    @Autowired
    public void globalConfigure(AuthenticationManagerBuilder auth) throws Exception{
        auth.userDetailsService(customUserDetailsService())
                .passwordEncoder(passwordEncoder());
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();

    }

    @Bean
    public CustomUserDetailsService customUserDetailsService(){
        return new CustomUserDetailsService();
    }
}
http.authorizeRequests().anyRequest().authenticated()
        .and()
        .formLogin().loginPage("/login_p")
        .loginProcessingUrl("/login")
        .usernameParameter("account")
        .passwordParameter("password")
        .permitAll()

loginPage方法指定登录界面,因为我们使用vue前后端分离,所以这里可以不用指定,可直接去掉

loginprocessingUrl方法设置登录接口,前端登录访问的接口,默认为/login。

usernameParameter和passwordParameter便是接口所需的参数。

.failureHandler(new AuthenticationFailureHandler() {
             @Override
             public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                 httpServletResponse.setContentType("application/json;charset=utf-8");
                 PrintWriter out = httpServletResponse.getWriter();                StringBuffer sb = new StringBuffer();
                 sb.append("{\"status\":\"error\",\"msg\":\"");
                 if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
                     sb.append("用户名或密码输入错误,登录失败!");
                 } else if (e instanceof DisabledException) {
                     sb.append("账户被禁用,登录失败,请联系管理员!");
                 } else {
                     sb.append("登录失败!");
                 }
                 sb.append("\"}");
                 out.write(sb.toString());
                 out.flush();
                 out.close();
             }

该方法主要是登录失败之后所进行的操作

.successHandler(new AuthenticationSuccessHandler() {
             @Override
             public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                 httpServletResponse.setContentType("application/json;charset=utf-8");
                 PrintWriter out = httpServletResponse.getWriter();
                 SecurityUser user = (SecurityUser) SecurityContextHolder.getContext()
                         .getAuthentication()
                         .getPrincipal();
                 Set<Menu> menus = new HashSet<>();
                 for(Role role : user.getRoles()){
                     menus.addAll(role.getMenus());
                 }
                 ArrayNode datas = JsonUtils.createArrayNode();
                 ObjectNode data;
                 data = datas.addObject();
                 data.put("account",user.getAccount());
                 data.put("userId",user.getsId());
                 data.put("username",user.getUsername());
                 ObjectMapper objectMapper = new ObjectMapper();
                 String s = "{\"status\":\"success\",\"msg\":" + objectMapper.writeValueAsString(datas) + "}";
                 out.write(s);
                 out.flush();
                 out.close();
             }
         })

登录成功后所进行的操作。

在登录中,我们还需要对密码进行加密,springSecurity有很多hash加密方法,在这里我们使用BCryptPasswordEncoder对密码进行加密

@Autowired
public void globalConfigure(AuthenticationManagerBuilder auth) throws Exception{
    auth.userDetailsService(customUserDetailsService())
            .passwordEncoder(passwordEncoder());
}

@Bean
public BCryptPasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();

}

 到了这一步,我们就基本完成了一个登录验证的功能,后面我会用vue+springboot完成一个拥有权限控制的系统。