超完整的springSecurity用户登录和权限认证(自定义)

2年前 (2022) 程序员胖胖胖虎阿
428 0 0
  1. 创建springboot工程,和引入springSecurity依赖、lombok依赖,mysql依赖,myBatisPlus依赖等相关依赖(根据自己所需)。
     
    但这不是我们的重点,这些相关配置自己实现创建就行,这里就不列出来。 

  2. 先看一下我的数据库里面的配置信息超完整的springSecurity用户登录和权限认证(自定义)

    上面是hr表,这将代表我们登录时候的用户超完整的springSecurity用户登录和权限认证(自定义)
    上面是role表,代表我们的用户权限
    超完整的springSecurity用户登录和权限认证(自定义)

    上面是hr_role表,把我们的hr和role联系起来 
    超完整的springSecurity用户登录和权限认证(自定义)

     这是menu表,代表我们请求url需要用户的什么权限
    超完整的springSecurity用户登录和权限认证(自定义)

    上面是menu_role表,把我门的menu和role联系起来
     
     

  3. 按照springSecurity的流程,我们先实现userDetailsService,覆写里面的用户认证方法

    @Service
    public class HrServiceImpl extends ServiceImpl<HrMapper, Hr> implements HrService, UserDetailsService {
    
        @Autowired
        private HrMapper hrMapper;
    
        //验证数据库中是否有这个员工
        @Override
        public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
            QueryWrapper wrapper = new QueryWrapper();
            wrapper.eq("username",s);
            Hr hr = hrMapper.selectOne(wrapper);
            if(hr==null){
                throw new UsernameNotFoundException("用户名不存在");
            }
            hr.setRoles(hrMapper.getRoles(s));
            return hr;
        }
    }

    再到我们的hr实体类中,它继承了userDetails接口,在这里我们把我们定义权限信息交给springSecurity

    //登录员工的类
    @Data
    @TableName("hr")
    public class Hr implements UserDetails {
        private int id;
        private String name;
        private String phone;
        private String telephone;
        private String address;
        private boolean enabled;
        private String username;
        private String password;
        private String remark;
        private String userface;
        @TableField(exist = false)
        private List<String> roles;
    
    
    
    
        //我们定义的role交给springSecurity
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            List<SimpleGrantedAuthority> authorities = new ArrayList<>();
            for (String role : roles){
                authorities.add(new SimpleGrantedAuthority(role));
            }
            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;
        }
    }

    OK,到这里,我们springSecurity的底层验证逻辑就写好了(通过数据库)。

  4. 接下来,就要自定义实现登录过滤器了LoginFilter

    public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    
        @Autowired
        SessionRegistry sessionRegistry;
        @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("verifyCode");
            //MediaType是主要告诉服务器request的资源类型
            if (request.getContentType().contains(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().contains(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
                Map<String, String> loginData = new HashMap<>();
                try {
                    //ObjectMapper是JackJson的重要类,可实现序列化和反序列化,下面是反序列化成Map对象
                    loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                } catch (IOException e) {
                }finally {
                    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);
                Hr principal = new Hr();
                principal.setUsername(username);
                sessionRegistry.registerNewSession(request.getSession(true).getId(), principal);
                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("验证码不正确");
            }
        }
    }

    这个过滤器,会将我们前端表单登录的信息,进行底层逻辑验证(用户名和密码),我们在这里面自己添加了一个校验验证码是否正确的方法。这里面还用到了JackJson,进行反序列化。相关依赖自己添加。

  5. 完成了LoginFilter后,我们就要实现根据当前url判断我现在的hr是否有权限能访问相关的资源,这里面我们覆写springsecurity的

    FilterInvocationSecurityMetadataSource 
    @Component
    public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    
        @Autowired
        MenuServiceImpl menuService;
    
        @Autowired
        MenuMapper menuMapper;
    
        AntPathMatcher antPathMatcher = new AntPathMatcher();
    
        @Override
        public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
            String requestUrl = ((FilterInvocation) o).getRequestUrl();
            List<Menu> menus = menuService.list();
            for (Menu menu : menus) {
                if (antPathMatcher.match(menu.getUrl(), requestUrl)) {
                    List<String> roles = menuMapper.getRoles(menu.getId());
                    String[] str = new String[roles.size()];
                    for (int i = 0; i < roles.size(); i++) {
                        str[i] = roles.get(i);
                    }
                    return SecurityConfig.createList(str);
                }
            }
            return SecurityConfig.createList("ROLE_LOGIN");
        }
    
        @Override
        public Collection<ConfigAttribute> getAllConfigAttributes() {
            return null;
        }
    
        @Override
        public boolean supports(Class<?> aClass) {
            return true;
        }
    }
    

     

  6. 完成好了配置,接下来就要教springSecurity跟我们定义好的资源来放行或者拦截

    @Component
    public class UrlDecisionManager implements AccessDecisionManager {
        @Override
        public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
            for (ConfigAttribute configAttribute : collection) {
                String needRole = configAttribute.getAttribute();
                if ("ROLE_LOGIN".equals(needRole)) {
                    if (authentication instanceof AnonymousAuthenticationToken) {
                        throw new AccessDeniedException("尚未登录,请登录!");
                    }else {
                        return;
                    }
                }
                Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
                for (GrantedAuthority authority : authorities) {
                    if (authority.getAuthority().equals(needRole)) {
                        return;
                    }
                }
            }
            throw new AccessDeniedException("权限不足,请联系管理员!");
        }
    
        @Override
        public boolean supports(ConfigAttribute configAttribute) {
            return true;
        }
    
        @Override
        public boolean supports(Class<?> aClass) {
            return true;
        }
    }

     

  7. 最后我们要到springSecurity统一的配置中心。相当于把我们自己定制好的SpringSecurity交给SpringBoot来管辖。

    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        HrServiceImpl hrService;
    
        @Autowired
        MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;
    
        @Autowired
        UrlDecisionManager urlDecisionManager;
    
        @Bean
        SessionRegistryImpl sessionRegistry() {
            return new SessionRegistryImpl();
        }
    
        //配置密码的编码格式
        @Bean
        PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        //不需要过滤的地址
        @Override
        public void configure(WebSecurity web){
            web.ignoring().antMatchers("/css/**", "/js/**", "/index.html", "/img/**", "/fonts/**", "/favicon.ico","/verifyCode");
        }
    
        //将我们的认证功能交给spring管理
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        //配置认证的实现方式
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(hrService).passwordEncoder(passwordEncoder());
        }
    
        @Bean
        LoginFilter loginFilter() throws Exception {
            LoginFilter loginFilter = new LoginFilter();
            loginFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
                        response.setContentType("application/json;charset=utf-8");
                        PrintWriter out = response.getWriter();
                        Hr hr = (Hr) authentication.getPrincipal();
                        hr.setPassword(null);
                        RespBean ok = new RespBean(200,"登录成功!", hr);
                        String s = new ObjectMapper().writeValueAsString(ok);
                        out.write(s);
                        out.flush();
                        out.close();
                    }
            );
            loginFilter.setAuthenticationFailureHandler((request, response, exception) -> {
                        response.setContentType("application/json;charset=utf-8");
                        PrintWriter out = response.getWriter();
                        RespBean respBean =  new RespBean(500,exception.getMessage(),null);
                        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");
            ConcurrentSessionControlAuthenticationStrategy sessionStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
            //用户登录的并发控制,只允许用户单点登录
            sessionStrategy.setMaximumSessions(1);
            loginFilter.setSessionAuthenticationStrategy(sessionStrategy);
            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(urlDecisionManager);
                            object.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);
                            return object;
                        }
                    })
                    .and()
                    .logout()
                    .logoutSuccessHandler((req, resp, authentication) -> {
                                resp.setContentType("application/json;charset=utf-8");
                                PrintWriter out = resp.getWriter();
                                out.write(new ObjectMapper().writeValueAsString(new RespBean(200,"注销成功!",null)));
                                out.flush();
                                out.close();
                            }
                    )
                    .permitAll()
                    .and()
                    .csrf().disable().exceptionHandling()
                    //没有认证时,在这里处理结果,不要重定向
                    .authenticationEntryPoint((req, resp, authException) -> {
                                resp.setContentType("application/json;charset=utf-8");
                                resp.setStatus(401);
                                PrintWriter out = resp.getWriter();
                                RespBean respBean = new RespBean(500,"访问失败!",null);
                                if (authException instanceof InsufficientAuthenticationException) {
                                    respBean.setMsg("请求失败,请联系管理员!");
                                }
                                out.write(new ObjectMapper().writeValueAsString(respBean));
                                out.flush();
                                out.close();
                            }
                    );
            http.addFilterAt(new ConcurrentSessionFilter(sessionRegistry(), event -> {
                HttpServletResponse resp = event.getResponse();
                resp.setContentType("application/json;charset=utf-8");
                resp.setStatus(401);
                PrintWriter out = resp.getWriter();
                out.write(new ObjectMapper().writeValueAsString(new RespBean(400,"您已在另一台设备登录,本次登录已下线!",null) ));
                out.flush();
                out.close();
            }), ConcurrentSessionFilter.class);
            http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
        }
    }

    至此,我们完成了整个自定义SpringSecurity的认证和授权体系。
    对了,下面是自己写的验证码生成。

    @Component
    public class VerificationCode {
    
        //设置验证码图片的宽、高
        private final int width = 100;
        private final int height = 30;
        private Image image;
        private String[] fontNames = new String[]{"微软雅黑","宋体","楷体","黑体"};
        //设置验证码的内容范围
        private String codes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        //验证码的内容
        private String text;
    
        /*
        * 设置随机code的颜色
        * */
        public Color getColor(){
            int Red = new Random().nextInt(150);
            int Blue = new Random().nextInt(150);
            int Green = new Random().nextInt(150);
            return new Color(Red,Green,Blue);
        }
    
        /*
        * 设置随机font的种类
        * */
        public Font getFont(){
            String name = fontNames[ new Random().nextInt(4) ];
            int style = new Random().nextInt(4);
            int size = new Random().nextInt(5)+24;
            return new Font(name,style,size);
        }
    
    
        /*
        * 创建一个验证码图片对象,
        * */
        public BufferedImage getImage(){
            //创建一个BufferImage,它可以在内存中操作
            BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_BGR);
            Graphics2D g2D = (Graphics2D) image.getGraphics();
            g2D.fillRect(0,0,width,height);
            //设置验证码的背景图案为白色
            g2D.setBackground(Color.white);
            StringBuilder text = new StringBuilder();
            //生成4个code形成验证码内容
            for(int i=0;i<4;i++){
                char r = codes.charAt( new Random().nextInt(62) );
                text.append(r);
                g2D.setColor( getColor() );
                g2D.setFont( getFont() );
                g2D.drawString(String.valueOf(r),i*25,height-8);
            }
            this.text = text.toString();
            return image;
        }
    
        /*
        *
        * */
        public String getText(){
            return this.text;
        }
    
    }
    
    @RestController
    public class LoginController {
    
        @GetMapping("/verifyCode")
        public void getCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
            VerificationCode vfCode = new VerificationCode();
            BufferedImage image = vfCode.getImage();
            String text = vfCode.getText();
            HttpSession session = request.getSession();
            session.setAttribute("verifyCode",text);
            ImageIO.write(image, "JPEG",response.getOutputStream() );
        }
    }
    
  8. 小细节补充了,接下来看看效果图:
    超完整的springSecurity用户登录和权限认证(自定义)

     超完整的springSecurity用户登录和权限认证(自定义)

     超完整的springSecurity用户登录和权限认证(自定义)

     根据地址栏,可以很清楚的看到我们已经由(/login)跳转到/home来了。但是我还没有写关于/home的资源controller。所以看不到界面。但是这篇文章主要是写SpringSecutiry的。

相关文章

暂无评论

暂无评论...