项目-21-Spring Security

Spring Security

介绍

  • 简介

    • Spring Security是一个专注与为Java应用程序提供身份认证和授权的框架,它的强大之处在于它可以轻松扩展以满足自定义的需求。
  • 特征

    • 对身份的认证和授权提供全面的、可扩展的支持。
    • 防止各种攻击,如会话固定攻击、点击劫持、csrf攻击等。
    • 支持与Servelt API、Spring MVC等Web技术集成。
  • 原理

    • 底层使用Filter(javaEE标准)进行拦截
    • Filter–>DispatchServlet–>Interceptor–>Controller(后三者属于Spring MVC)
  • 推荐学习网站:

    www.spring4all.com

    • 看几个核心的Filter源码

使用

  • 1.导包:spring-boot-starter-security
 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
 </dependency>

废弃登录的拦截器

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private AlphaInterceptor alphaInterceptor;
    @Autowired
    private LoginTicketInterceptor loginTicketInterceptor;
    //@Autowired
    //private LoginRequiredInterceptor loginRequiredInterceptor;
    @Autowired
    private MessageInterceptor messageInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(alphaInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
                .addPathPatterns("/register", "/login");

        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");
    }
}
  • 2.新建配置类SecurityConfig

    • 2.1 添加权限信息[CommunityConstant]
        /**
         * 权限管理:普通用户
         */
        String AUTHORITY_USER = "user";
        /**
         * 权限管理:管理员
         */
        String AUTHORITY_ADMIN = "admin";
        /**
         * 权限管理:版主
         */
        String AUTHORITY_MODERATOR = "moderator";
    
    • 2.2 SecurityConfig
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.AuthenticationEntryPoint;
    import org.springframework.security.web.access.AccessDeniedHandler;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    
    //Security的配置类
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter implements CommunityConstant {
    
        // 1. 忽略静态资源,不对静态资源进行拦截
        @Override
        public void configure(WebSecurity web) throws Exception {
            // 忽略resources目录下的资源
            web.ignoring().antMatchers("/resources/**");
        }
    
        // 2. 授权
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    // 2.1 对于以下列出的所有路径
                    .antMatchers(
                            "/user/setting", // 用户设置
                            "/user/upload", // 用户文件上传
                            "/discuss/add", // 帖子发布
                            "/comment/add/**", // 评论发布
                            "/letter/**", // 私信相关内容
                            "/notice/**", // 通知相关内容
                            "/like", // 点赞
                            "/follow", // 加关注
                            "/unfollow" // 取消关注
                    )
                    // 只要有以下相关权限,都可以访问 [登录]
                    .hasAnyAuthority(
                            AUTHORITY_USER,// 权限: 普通用户
                            AUTHORITY_ADMIN,// 权限: 管理员
                            AUTHORITY_MODERATOR// 权限: 版主
                    )
                    // 2.2 对于以下列出的所有路径
                    .antMatchers(
                            "/discuss/top",
                            "/discuss/wonderful"
                    )
                    // 只有具有以下列出的权限才可以访问
                    .hasAnyAuthority(
                            AUTHORITY_MODERATOR// 权限: 版主
                    )
                    // 2.3 对于以下列出的所有路径
                    .antMatchers(
                            "/discuss/delete",
                            "/data/**"
                    )
                    // 只有具有以下列出的权限才可以访问
                    .hasAnyAuthority(
                            AUTHORITY_ADMIN
                    )
                    // 除了以上列出的权限限制约定外,其他请求路径都放行
                    .anyRequest().permitAll()
                    //
                    .and().csrf().disable();
    
            // 权限不够时
            // 如果权限不够时的处理
            http.exceptionHandling()
                    // 没有登录时的处理
                    .authenticationEntryPoint(new AuthenticationEntryPoint() {
                        // 没有登录时的处理方法
                        @Override
                        public void commence(HttpServletRequest request,
                                             HttpServletResponse response,
                                             AuthenticationException e)
                                throws IOException, ServletException {
                            // 如果请求x-requested-with 中头包含XMLHttpRequest 说明是异步请求
                            String xRequestedWith = request.getHeader("x-requested-with");
                            if ("XMLHttpRequest".equals(xRequestedWith)) {
                                // 设置响应体是json 格式(因为是异步请求,所以返回内容要是json格式)
                                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)) {
                                // 设置响应体是json 格式(因为是异步请求,所以返回内容要是json格式)
                                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");
        }
    }
    
    • 2.3 在UserService中添加获取用户权限的方法
    	//用于验证用户的权限: 根据userId获取用户的权限
        public Collection<? extends GrantedAuthority> getAuthorities(int userId){
            //1. 根据ID获取用户
            User user = this.findUserById(userId);
            //2. 根据user判断用户的权限
            List<GrantedAuthority> list = new ArrayList<>();
            list.add(new GrantedAuthority() {
                @Override
                public String getAuthority() {
                    // 根据用户的type确定用户的权限
                    switch (user.getType()){
                        case 1:
                            return AUTHORITY_ADMIN;
                        case 2:
                            return AUTHORITY_MODERATOR;
                        default:
                            return AUTHORITY_USER;
                    }
                }
            });
            return list;
        }
    
    • 2.4 在LoginTicketInterceptor

    因为,SpringSecurity自己可以实现认证和权限,但是由于我们已经在系统中构建了认证的逻辑。因此,我们需要绕过SpringSecurity的认证,只利用权限。我们需要自己获取用户认证的结果,将认证结果存入到SecurityContext中,这是因为Security进行授权需要获取相关内容。

    	@Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 从cookie中获取凭证
            String ticket = CookieUtil.getValue(request, "ticket");
    
            if (ticket != null) {
                // 查询凭证
                LoginTicket loginTicket = userService.findLoginTicket(ticket);
                // 检查凭证是否有效
                if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
                    // 根据凭证查询用户
                    User user = userService.findUserById(loginTicket.getUserId());
                    // 在本次请求中持有用户
                    hostHolder.setUser(user);
                    // 构建用户认证的结果,并存入SecurityContext中,便于Security进行授权
                    // UsernamePasswordAuthenticationToken(用户,密码,权限)
                    Authentication authentication = new UsernamePasswordAuthenticationToken(
                            user,user.getPassword(),userService.getAuthorities(user.getId()));
                    SecurityContextHolder.setContext(new SecurityContextImpl(authentication));
                }
            }
            return true;
        }
    	//清理权限
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            hostHolder.clear();
            // 清理SecurityContextHolder
            SecurityContextHolder.clearContext();  
        }
    
    • 2.5 LoginController的退出方法中将用户认证的结果清除
        @RequestMapping(path = "/logout", method = RequestMethod.GET)
        public String logout(@CookieValue("ticket") String ticket) {
            userService.logout(ticket);
            // 清理SecurityContextHolder
            SecurityContextHolder.clearContext();
            return "redirect:/login";
        }
    
    • 3.CSRF配置

    防止CSRF攻击的基本原理,以及表单、AJAX的相关配置。

    • CSRF攻击:某网站盗取你的Cookie(ticket)凭证,模拟你的身份访问服务器。(发生在提交表单的时候)
    • 首先浏览器已经与服务器建立连接,服务器已经将登录凭证存在浏览器的cookie中
    • 当浏览器再次向服务器发起请求,一般是获取表单;服务器正常响应表单至浏览器
    • 但是某网站盗取浏览器的Cookie(ticket)凭证,模拟你的身份访问服务器,提交表单,造成CSRF攻击

    解决:

    • Security会在表单里增加一个TOCKEN(自动生成),恶意网站虽然可以获取cookie但无法获取TOCKEN,因此保证了安全
    • 但是异步请求Security无法在html文件生成CSRF令牌(异步不是通过请求体传数据,通过请求头)
    • 发送AJAX请求之前,将CSRF令牌设置到请求的消息头中

解决:利用Security添加Tocken

  • 3.1 处理异步请求

    index.html

    	<!--访问该页面时,生成CSRF令牌-->
    	<meta name="_csrf" th:content="${_csrf.token}">  <!--生成CSRF令牌的value-->
    	<meta name="_csrf_header" th:content="${_csrf.headerName}"><!--生成CSRF令牌的key-->
    

    index.js

    	//发送AJAX请求之前, 将CSRF令牌设置到请求的消息头中
        var token = $("meta[name='_csrf']").attr("content");
        var header = $("meta[name='_csrf_header']").attr("content");
        $(document).ajaxSend(function(e, xhr, options){
            xhr.setRequestHeader(header, token);
        });
    

    可以在config配置取消CSRF

     // 取消CSRF
     .and().csrf().disable();
    
上一篇:关于英语中对于权限的几个单词做个介绍


下一篇:oracle 相关