Spring Security登录验证(项目)

Spring Security登录验证

背景

本人最近微人事项目进行了学习,记录一下学习的过程,总结一下。

首先,这个微人事项目是为了事业单位的人事管理,包括用户登录的权限管理、员工基本资料的增删查改,薪资管理、部门管理等等。

接下来总结下权限管理:

权限数据库设计

这里需要建立5个数据表,分别是资源表、角色表、用户表、资源角色表和用户角色表。

资源表menu

这是登录之后前端界面可以显示的模块。

Spring Security登录验证(项目)_第1张图片
角色表role

认证采用的Spring-Security,所以,英文名需要以ROLE_开头。

Spring Security登录验证(项目)_第2张图片
用户表hr

该表统计用户的基本信息。
Spring Security登录验证(项目)_第3张图片
资源角色表menu_role

这里的mid对应于资源表的idrid对应于角色表的id。这里就是为了实现根据登录的用户的角色动态显示前端页面的资源信息,即实现不同的用户有不同的权限。
Spring Security登录验证(项目)_第4张图片
用户角色表hr_role
这里的hrid对应于用户表中的idrid对应于角色表里的id,可以实现用户多角色的功能。
Spring Security登录验证(项目)_第5张图片

角色与资源的关系

其中hr用户表实现了UserDetails接口

除了用户的基本信息外,

// 用于账户是否过期
	@Override
    public boolean isAccountNonExpired() {
        return true;
    }
// 账户是否被锁住
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
//密码是否过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
//密码是否被禁用
    @Override
    public boolean isEnabled() {
        return enabled;
    }

//一个用户对应于多个角色,统计用户的角色进行返回
    @Override
    @JsonIgnore
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>(roles.size());
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

HrService实现了UserDetailsService。用于执行登录。

重写了loadUserByUsername

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
    Hr hr = hrMapper.loadUserByUsername(username);
    if(hr == null){
        throw new UsernameNotFoundException("用户名不存在!");
    }
    //设置当前用户所具有的角色
    hr.setRoles(hrMapper.getHrRolesById(hr.getId()));
    return hr;
}

讲一下登录

FilterInvocationSecurityMetadataSource,此类的功能是通过当前的请求地址,获取该地址需要的角色。

@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    MenuService menuService;
    AntPathMatcher antPathMatcher = new AntPathMatcher();
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        List<Menu> menus = menuService.getAllMenusWithRole();
        for (Menu menu : menus) {
            if (antPathMatcher.match(menu.getUrl(), requestUrl)) {
                List<Role> roles = menu.getRoles();
                String[] str = new String[roles.size()];
                for (int i = 0; i < roles.size(); i++) {
                    str[i] = roles.get(i).getName();
                }
                return SecurityConfig.createList(str);
            }
        }
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

getAttribute(Object o)方法中的参数o中提取出当前请求的URL,然后将这个请求和数据库查询出来的URL-pattern进行比较,看符合哪一个URL-pattern,就获取到该URL-pattern所对应的角色。这时的角色可能有多个角色,需要进行遍历,利用SecurityConfig.createList()方法创建一个角色集合。当输入的URL和数据库中查询的不对应,需要进行登录ROLE_LOGIN

AccessDecisionManager,从FilterInovationSecurityMetadataSource中的getAttribute()返回的集合会来到这里。

@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        for (ConfigAttribute configAttribute : configAttributes) {
            String needRole = configAttribute.getAttribute();
            if ("ROLE_LOGIN".equals(needRole)) {
                if (authentication instanceof AnonymousAuthenticationToken) {
                    throw new AccessDeniedException("尚未登录,请登录!");
                }else {
                    return;
                }
            }
            //把已经被认证的的角色与needRole进行比较
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足,请联系管理员!");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

在这个类中有个decide()方法,有3个参数,第一个参数保存了当前登录用户的角色信息,第3个参数是由FilterInvocationSecurityMetadataSourcegetAttributes()方法传来的,表示当前登录用户的角色是否能和数据库查询的角色相匹配。

当登录用户的角色和经过FilterInvocationSecurityMetadataSource处理的角色匹配得上时,进行返回。

配置SecurityConfig

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    HrService hrService;
    @Autowired
    CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;
    @Autowired
    CustomUrlDecisionManager customUrlDecisionManager;

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //认证管理器配置方法
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //和用户相关的都进行配置
        auth.userDetailsService(hrService);
    }

    // 核心过滤器配置方法
    @Override
    public void configure(WebSecurity web) throws Exception {
        //忽略规则配置文件。
    web.ignoring().antMatchers("/css/**","/js/**","/index.html","/img/**","/fonts/**","/favicon.ico","/verifyCode");
    }

    @Bean
    LoginFilter loginFilter() throws Exception {
        LoginFilter loginFilter = new LoginFilter();
        //登录成功时
        loginFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                //设置Body的类型。
                response.setContentType("application/json;charset=utf-8");
                //写入响应,获取一个字符流。
                PrintWriter out = response.getWriter();
                //获取授权后的用户信息
                Hr hr = (Hr) authentication.getPrincipal();
                hr.setPassword(null);
                RespBean ok = RespBean.ok("登录成功!", hr);
                String s = new ObjectMapper().writeValueAsString(ok);
                out.write(s);
                out.flush();
                out.close();
            }
        });
        //登录失败
        loginFilter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                response.setContentType("application/json;charset=utf-8");
                PrintWriter out = response.getWriter();
                RespBean respBean = RespBean.error(exception.getMessage());
                if (exception instanceof LockedException) {
                    respBean.setMsg("账户被锁定,请联系管理员!");
                } else if (exception instanceof CredentialsExpiredException) {
                    respBean.setMsg("密码过期,请联系管理员!");
                } else if (exception instanceof AccountExpiredException) {
                    respBean.setMsg("账户过期,请联系管理员!");
                } else if (exception instanceof DisabledException) {
                    respBean.setMsg("账户被禁用,请联系管理员!");
                } else if (exception instanceof BadCredentialsException) {
                    respBean.setMsg("用户名或者密码输入错误,请重新输入!");
                }
                out.write(new ObjectMapper().writeValueAsString(respBean));
                out.flush();
                out.close();
            }
        });
        loginFilter.setAuthenticationManager(authenticationManagerBean());
        loginFilter.setFilterProcessesUrl("/doLogin");
        return loginFilter;
    }

    //安全过滤器链配置方法
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setAccessDecisionManager(customUrlDecisionManager);
                        object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
                        return object;
                    }
                })
                .and()
                .logout()
                .logoutSuccessHandler(new LogoutSuccessHandler() {
                    @Override
                    public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter out = resp.getWriter();
                        out.write(new ObjectMapper().writeValueAsString(RespBean.ok("注销成功!")));
                        out.flush();
                        out.close();
                    }
                })
                .permitAll()
                .and()
                .csrf().disable().exceptionHandling()
                //没有认证时,在这里处理结果,不要重定向
                .authenticationEntryPoint(new AuthenticationEntryPoint() {
            @Override
            public void commence(HttpServletRequest req, HttpServletResponse resp, AuthenticationException authException) throws IOException, ServletException {
                resp.setContentType("application/json;charset=utf-8");
                resp.setStatus(401);
                PrintWriter out = resp.getWriter();
                RespBean respBean = RespBean.error("访问失败!");
                if (authException instanceof InsufficientAuthenticationException) {
                    respBean.setMsg("请求失败,请联系管理员!");
                }
                out.write(new ObjectMapper().writeValueAsString(respBean));
                out.flush();
                out.close();
            }
        });
        http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

SecurityConfig配置类中有LoginFilterBean对象。看看它都有哪些东西

public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {//向服务器发送信息。
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        //从session中获取之前存入的验证码。
        String verify_code = (String) request.getSession().getAttribute("verify_code");
        //处理json类型的数据
        if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
            Map<String, String> loginData = new HashMap<>();
            try {
                loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
            } catch (IOException e) {
            }finally {
                //把用户输入的验证码和session里面的进行比较
                String code = loginData.get("code");
                checkCode(response, code, verify_code);
            }
            //得到用户名
            //得到密码
            String username = loginData.get(getUsernameParameter());
            String password = loginData.get(getPasswordParameter());
            if (username == null) {
                username = "";
            }
            if (password == null) {
                password = "";
            }
            username = username.trim();
            //身份验证
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    username, password);
            setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        } else {
            checkCode(response, request.getParameter("code"), verify_code);
            return super.attemptAuthentication(request, response);
        }
    }

    public void checkCode(HttpServletResponse resp, String code, String verify_code) {
        if (code == null || verify_code == null || "".equals(code) || !verify_code.toLowerCase().equals(code.toLowerCase())) {
            //验证码不正确
            throw new AuthenticationServiceException("验证码不正确");
        }
    }
}

你可能感兴趣的:(Spring)