8.Spring Security 权限控制

该文详细介绍了如何在JavaEE和SpringMVC项目中集成SpringSecurity进行用户认证和权限控制。通过实现UserDetails接口,自定义AuthenticationProvider以及配置SecurityConfig,实现了登录、注销、权限分配等功能。同时,文章提到了如何处理验证码和异常情况,以及权限不足时的拦截策略。

1.简介入门

JavaEE和SpringMVC :Spring Security就是通过11个Fliter进行组合管理

小Demo

user实体类

  • user.type字段,0普通用户,1超级管理员,2版主

  • 补全get set tostring

  • implement UserDetails,重写以下方法

// true: 账号未过期.
@Override
public boolean isAccountNonExpired() {
    return true;
}

// true: 账号未锁定.
@Override
public boolean isAccountNonLocked() {
    return true;
}

// true: 凭证未过期.
@Override
public boolean isCredentialsNonExpired() {
    return true;
}

// true: 账号可用.
@Override
public boolean isEnabled() {
    return true;
}

//获取当前用户权限列表
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    List<GrantedAuthority> list = new ArrayList<>();
    list.add(new GrantedAuthority() {
        @Override
        public String getAuthority() {
            switch (type) {
                case 1:
                    return "ADMIN";
                default:
                    return "USER";
            }
        }
    });
    return list;
}

UserService

unservice implement UserDetailsService

  • 重写方法loadUserByUsername

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    return this.findUserByName(username);
}

SecurityConfig 配置类

  • extend父类WebSecurityConfigurerAdapter

  • 注入UserService

  • 重写configure(WebSecurity web),忽略静态资源的访

@Override
public void configure(WebSecurity web) throws Exception {
    // 忽略静态资源的访问
    web.ignoring().antMatchers("/resources/**");
}
  • 重写configure(AuthenticationManagerBuilder auth),这个方法主要是做认证。

  • AuthenticationManager: 认证的核心接口

  • AuthenticationManagerBuilder: 用于构建AuthenticationManager对象的工具

  • ProviderManager: AuthenticationManager接口的默认实现类

  • ProviderManager--一组-->AuthenticationProvider--每个负责-->一种认证

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    // 内置的认证规则
    // auth.userDetailsService(userService).passwordEncoder(new Pbkdf2PasswordEncoder("12345"));

    // 自定义认证规则
    // AuthenticationProvider: ProviderManager持有一组AuthenticationProvider,每个AuthenticationProvider负责一种认证.
    // 委托模式: ProviderManager将认证委托给AuthenticationProvider.
    auth.authenticationProvider(new AuthenticationProvider() {
        // Authentication: 用于封装认证信息的接口,不同的实现类代表不同类型的认证信息.
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            String username = authentication.getName();
            String password = (String) authentication.getCredentials();

            User user = userService.findUserByName(username);
            if (user == null) {
                throw new UsernameNotFoundException("账号不存在!");
            }

            password = CommunityUtil.md5(password + user.getSalt());
            if (!user.getPassword().equals(password)) {
                throw new BadCredentialsException("密码不正确!");
            }

            // principal: 主要信息; credentials: 证书; authorities: 权限;
            return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
        }

        // 当前的AuthenticationProvider支持哪种类型的认证.
        @Override
        public boolean supports(Class<?> aClass) {
            // UsernamePasswordAuthenticationToken: Authentication接口的常用的实现类.
            return UsernamePasswordAuthenticationToken.class.equals(aClass);
        }
    });
}
  • 重写configure(HttpSecurity http)

配置登陆页面http.formLogin()

登录成功处理器.successHandler

登录失败处理器.failureHandler

退出相关配置http.logout()

授权配置http.authorizeRequests()

验证码在验证账号之前

 @Override
protected void configure(HttpSecurity http) throws Exception {
    // 登录相关配置
    http.formLogin()
            .loginPage("/loginpage")
            .loginProcessingUrl("/login")
            .successHandler(new AuthenticationSuccessHandler() {
                @Override
                public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                    response.sendRedirect(request.getContextPath() + "/index");
                }
            })
            .failureHandler(new AuthenticationFailureHandler() {
                @Override
                public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
                    request.setAttribute("error", e.getMessage());
                    request.getRequestDispatcher("/loginpage").forward(request, response);
                }
            });

    // 退出相关配置
    http.logout()
            .logoutUrl("/logout")
            .logoutSuccessHandler(new LogoutSuccessHandler() {
                @Override
                public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                    response.sendRedirect(request.getContextPath() + "/index");
                }
            });

    // 授权配置
    http.authorizeRequests()
            .antMatchers("/letter").hasAnyAuthority("USER", "ADMIN")
            .antMatchers("/admin").hasAnyAuthority("ADMIN")
            .and().exceptionHandling().accessDeniedPage("/denied");

    // 增加Filter,处理验证码
    http.addFilterBefore(new Filter() {
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            HttpServletResponse response = (HttpServletResponse) servletResponse;
            if (request.getServletPath().equals("/login")) {
                String verifyCode = request.getParameter("verifyCode");
                if (verifyCode == null || !verifyCode.equalsIgnoreCase("1234")) {
                    request.setAttribute("error", "验证码错误!");
                    request.getRequestDispatcher("/loginpage").forward(request, response);//转发
                    return;
                }
            }
            // 让请求继续向下执行.
            filterChain.doFilter(request, response);
        }
    }, UsernamePasswordAuthenticationFilter.class);

    // 记住我
    http.rememberMe()
            .tokenRepository(new InMemoryTokenRepositoryImpl())
            .tokenValiditySeconds(3600 * 24)
            .userDetailsService(userService);

}

HomeController

在首页添加欢迎信息,通过SecurityContextHolder获取登陆者信息

@RequestMapping(path = "/index", method = RequestMethod.GET)
public String getIndexPage(Model model) {
    // 认证成功后,结果会通过SecurityContextHolder存入SecurityContext中.
    Object obj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    if (obj instanceof User) {
        model.addAttribute("loginUser", obj);
    }
    return "/index";
}

2.权限控制

  • 废除原有的拦截器

config.WebMvcConfig 种注释掉两部分

@Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginTicketInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
       // registry.addInterceptor(loginRequiredInterceptor)
       //         .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
        registry.addInterceptor(messageInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
   }
  • 配置授权SecurityConfig

  1. 重写configure(HttpSecurity http),忽略对静态资源的拦截

  1. 重写configure(HttpSecurity http)http.authorizeRequests()进行授权

  1. .antMatchers:登陆后可访问路径

  1. hasAnyAuthority:可以访问的权限

  1. .anyRequest().permitAll():其他请求都允许

  1. http.exceptionHandling():越权行为发生时

  1. 覆盖它默认的logout逻辑,才能执行我们自己的退出代码

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 授权
    http.authorizeRequests()
            .antMatchers(
                    "/user/setting",
                    "/user/upload",
                    "/discuss/add",
                    "/comment/add/**",
                    "/letter/**",
                    "/notice/**",
                    "/like",
                    "/follow",
                    "/unfollow"
            )
            .hasAnyAuthority(
                    AUTHORITY_USER,
                    AUTHORITY_ADMIN,
                    AUTHORITY_MODERATOR
            )
            .anyRequest().permitAll()

    // 权限不够时的处理
    http.exceptionHandling()
            .authenticationEntryPoint(new AuthenticationEntryPoint() {
                // 没有登录
                @Override
                public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
                    String xRequestedWith = request.getHeader("x-requested-with");
                    if ("XMLHttpRequest".equals(xRequestedWith)) {
                        response.setContentType("application/plain;charset=utf-8");
                        PrintWriter writer = response.getWriter();
                        writer.write(CommunityUtil.getJSONString(403, "你还没有登录哦!"));
                    } else {
                        response.sendRedirect(request.getContextPath() + "/login");
                    }
                }
            })
            .accessDeniedHandler(new AccessDeniedHandler() {
                // 权限不足
                @Override
                public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
                    String xRequestedWith = request.getHeader("x-requested-with");
                    if ("XMLHttpRequest".equals(xRequestedWith)) {
                        response.setContentType("application/plain;charset=utf-8");
                        PrintWriter writer = response.getWriter();
                        writer.write(CommunityUtil.getJSONString(403, "你没有访问此功能的权限!"));
                    } else {
                        response.sendRedirect(request.getContextPath() + "/denied");
                    }
                }
            });
    
    // Security底层默认会拦截/logout请求,进行退出处理.
    // 覆盖它默认的逻辑,才能执行我们自己的退出代码.
    http.logout().logoutUrl("/securitylogout");
}

  • UserService增加用户权限

public Collection<? extends GrantedAuthority> getAuthorities(int userId) {
    User user = this.findUserById(userId);

    List<GrantedAuthority> list = new ArrayList<>();
    list.add(new GrantedAuthority() {

        @Override
        public String getAuthority() {
            switch (user.getType()) {
                case 1:
                    return AUTHORITY_ADMIN;
                case 2:
                    return AUTHORITY_MODERATOR;
                default:
                    return AUTHORITY_USER;
            }
        }
    });
    return list;
}
  • 修改LoginTicketInterceptor

BUG: 登陆后点击其他需要授权的页面 依然会跳转到登录页面?
问题:在 afterCompletion
@RequestMapping(path = "/logout", method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket) {
userService.logout(ticket);
SecurityContextHolder.clearContext();
return "redirect:/login";
}
原因: security包括认证和授权,授权是根据认证的结果来进行的。
这里我们没有使用框架的认证,而采用自己的认证:其实就是保存一下用户的权限,就是将user信息存入 ThreadLocal 和
SecurityContextHolder.setContext
第一次login之后,已经经过interceptor了,请求处理完后 会执行SecurityContextHolder.clearContext() ,会清除user信息。下次再访问有权限的路径,就需要认证,但此时还没有用户信息,所以需要登录。

preHandle

将得到的结果存入SecurityContext

@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //从cookie获取 ticket封装好cookie
        String ticket = CookieUtil.getValue(request,"ticket");  //登录时 返回了Cookie cookie = new Cookie("ticket",map.get("tivket").toString());
        if(ticket != null){
            //查询 登陆凭证ticket
            LoginTicket loginTicket = userService.findLoginTicket(ticket);
            //判断是否有效+否过期
            if(loginTicket != null &&loginTicket.getStatus() == 0 &&loginTicket.getExpired().after(new Date())){
                //根据 loginTicket 查询用户
                User user = userService.findUserById(loginTicket.getUserId());
                //把用户信息 暂存  每个浏览器访问服务器时 服务器会创建单独的线程来执行请求 即多线程的环境 考虑线程隔离;
                hostHolder.setUser(user);

                // 构建用户认证的结果,并存入SecurityContext,以便于Security进行授权.
                Authentication authentication = new UsernamePasswordAuthenticationToken(
                        user, user.getPassword(), userService.getAuthorities(user.getId()));

                SecurityContextHolder.setContext(new SecurityContextImpl(authentication));

                System.out.println(SecurityContextHolder.getContext());
            }
        }
        return true;
    }
  • LoginController

@RequestMapping(path = "/logout", method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket) {
    userService.logout(ticket);
    SecurityContextHolder.clearContext();
    return "redirect:/login";
}

3.加精 置顶 删除

  • DAODiscussPostMapper添加 修改类型 状态 的方法

  • 完善对应的Mapper.xml

  • DiscussPostController

置顶请求

@RequestMapping(path = "/top", method = RequestMethod.POST)
    @ResponseBody
    public String setTop(int id){
        DiscussPost discussPostById = discussPostService.findDiscussPostById(id);
        // 获取置顶状态,1为置顶,0为正常状态,1^1=0 0^1=1
        int type = discussPostById.getType()^1;
        discussPostService.updateType(id, type);
        // 返回的结果
        Map<String, Object> map = new HashMap<>();
        map.put("type", type);

        // 触发发帖事件(更改帖子状态)
        Event event = new Event()
                .setTopic(TOPIC_PUBLISH)
                .setUserId(hostHolder.getUser().getId())
                .setEntityType(ENTITY_TYPE_POST)
                .setEntityId(id);
        eventProducer.fireEvent(event);

        return CommunityUtil.getJSONString(0, null, map);
    }

加精类似

删除 直接将status 改为 2(拉黑 不显示)

  • 配置 SecurityConfig 权限情况

                .antMatchers(
                        "/discuss/top",
                        "/discuss/wonderful"
                )
                .hasAnyAuthority(
                        AUTHORITY_MODERATOR
                )
                .antMatchers(
                        "/discuss/delete",
                        "/data/**"
                )
                .hasAnyAuthority(
                        AUTHORITY_ADMIN
                )

                .anyRequest().permitAll()
                .and().csrf().disable();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值