Java牛客项目课_仿牛客网讨论区_第七章

Posted 夜中听雪

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java牛客项目课_仿牛客网讨论区_第七章相关的知识,希望对你有一定的参考价值。

第七章、项目进阶,构建安全高效的企业服务

7.1、Spring Security(原型项目上加Spring Security,不是在真正的项目上更改)

在这里插入图片描述
Spring Security底层用11个Filter来做权限控制之类,如果你没登录,你连DispatcherServlet都访问不了,就更不用说Controller了。
Filter和DispatcherServlet都是JavaEE的标准,是由SpringMVC实现的。Interpector和Controller是SpringMVC自己的。
老师建议研究SpringSecurity的源码,因为1、这个组件在SpringCloud、SpringBoot,即Spring家族中都能用,研究它,你会扩展的研究其他组件。2、它很复杂,在Spring家族中复杂程度靠前,是块硬骨头,建议先啃。
老师推荐这个网站Spring For All 玩最纯粹的技术!做最专业的 Spring 民间组织~学SpringSecurity:社区 Spring Security 从入门到进阶系列教程 | Spring For All
因为是中文的,而且文章质量普遍高。
研究源码要打断点跟一下流程。但不要每个源码都跟,光跟一下核心类就好。
比如SpringSecurity中的几个核心Filter。

重定向:
在这里插入图片描述
转发:
在这里插入图片描述

区别:
1、
前者是两个独立的功能,没有耦合,比如删除某帖子,然后重定向到主页,即查询所有帖子。
后者是需要两个组件实现一个请求,有耦合,例如图片中例子,登录表单提交到“/login”,然后发现登录失败,就携带错误信息转发给去模板页面的地址“/loginpage”,如果在Controller里,是可以把参数放model里直接转给模板的,但是http.formLogin().failureHandler()不在Controller里。转发比转给模板更灵活,因为可以复用“/loginpage”里的一些逻辑
2、地址栏的地址显示不同

SecurityConfig.java类
注释:
authentication,认证
authorization,授权

package com.nowcoder.community.config;

import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.UserService;
import com.nowcoder.community.util.CommunityUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 忽略静态资源的访问
        web.ignoring().antMatchers("/resources/**");
    }

    //authentication,认证
    // AuthenticationManager: 认证的核心接口.
    // AuthenticationManagerBuilder: 用于构建AuthenticationManager对象的工具.
    // ProviderManager: AuthenticationManager接口的默认实现类.
    @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);
            }
        });
    }

    //authorization,授权
    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
        //super.configure(http);//覆盖该方法
        // 登录相关配置
        http.formLogin()
                .loginPage("/loginpage")//跳转到登录页面的路径,见HomeController
                .loginProcessingUrl("/login")//登录表单提交到哪个路径的Controller
                //.successForwardUrl()//成功时跳转到哪里。但由于我们要除了一些逻辑,跳转时还要携带一些参数,于是使用下方的.successHandler()会更灵活。
                //.failureForwardUrl()//失败时跳转到哪里。同理。
                .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")//只要你拥有"USER", "ADMIN"中任何一个权限,你就可以访问私信"/letter"页面
                .antMatchers("/admin").hasAnyAuthority("ADMIN")
                .and().exceptionHandling().accessDeniedPage("/denied");//如果权限不匹配,就跳转到"/denied"页面

        //验证码应该在账号密码处理之前先处理,如果验证码都不对,就不用看账号密码了。所以要在验证账号密码的Filter之前增加一个验证验证码的Filter验证
        // 增加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);//验证码对了,才不会return,才会走到这里,请求才会继续向下执行.
            }
        }, UsernamePasswordAuthenticationFilter.class);//新的这个new Filter,要在UsernamePasswordAuthenticationFilter这个Filter之前过滤

        /*
        如果勾选了"记住我",Spring Security会往浏览器里存一个cookie,cookie里存着user的用户名,
         然后,关掉浏览器/关机,下次再访问时,浏览器把cookie传给服务器,服务器根据用户名和userService查出该用户user,
         然后,会通过SecurityContextHolder把user存入SecurityContext中,
         然后,用户访问"/index"页面时,会从SecurityContext取出user的用户名,然后显示在主页上.
         */
        // 记住我
        http.rememberMe()
                .tokenRepository(new InMemoryTokenRepositoryImpl())//如果你想把数据存到Redis/数据库里,那么就自己实现TokenRepository接口,然后.tokenRepository(tokenRepository)这样
                .tokenValiditySeconds(3600 * 24)//24小时
                .userDetailsService(userService);//必须有

    }
}

部分index.html代码

        <!--SpringSecurity规定,退出必须使用post请求。第一个<li>是get请求。第二个<li>是post请求,post请求必须使用form表单。-->
        <!--<li><a th:href="@{/loginpage}">退出</a></li>-->
        <li>
            <form method="post" th:action="@{/logout}">
                <a href="javascript:document.forms[0].submit();">退出</a>
            </form>
        </li>

部分HomeController.java代码

    @RequestMapping(path = "/index", method = RequestMethod.GET)
    public String getIndexPage(Model model) {
        // 认证成功后,结果会通过SecurityContextHolder存入SecurityContext中.
        // return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());这是存入的东西,取出principal会取出user
        Object obj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if (obj instanceof User) {
            model.addAttribute("loginUser", obj);
        }
        return "/index";
    }

login.html部分代码

    <form method="post" th:action="@{/login}">
        <p style="color:#ff0000;" th:text="${error}">
            <!--提示信息-->
        </p>
        <!--name=""中的:username,password,remember-me.这三个名字是SpringSecurity固定的.-->
        <p>
            账号:<input type="text" name="username" th:value="${param.username}"><!--在登录失败回到登录页面时,要有上次登录的账号密码回填-->
        </p>
        <p>
            密码:<input type="password" name="password" th:value="${param.password}">
        </p>
        <p>
            验证码:<input type="text" name="verifyCode"> <i>1234</i>
        </p>
        <p>
            <input type="checkbox" name="remember-me"> 记住我
        </p>
        <p>
            <input type="submit" value="登录">
        </p>
    </form>

7.3、权限控制(这是在真正的项目上更改,增加了SpringSecurity)

牛客课程助教 V 助教 回复 Eric.Lee :

  1. Security提供了认证和授权两个功能,我们在DEMO里也做了演示,而在项目中应用时,我们并没有使用它的 认证功能,而单独的使用了它的授权功能,所以需要对认证的环节做一下特殊的处理,以保证授权的正常进行;
  2. Security的所有功能,都是基于Filter实现的,而Filter的执行早于Interceptor和Controller,关于Security的Filter原理,可以参考http://www.spring4all.com/article/458;
  3. 我们的解决方案是,在Interceptor中判断登录与否,然后人为的将认证结果添加到了SecurityContextHolder里。这里要注意,由于Interceptor执行晚于Filter,所以认证的进行依赖于前一次请求的Interceptor处理。比如,我登录成功了,然后请求自行重定向到了首页。在访问首页时,认证Filter其实没起作用,因为这个请求不需要权限,然后执行了Interceptor,此时才将认证结果加入SecurityContextHolder,这时你再访问/letter/list,可以成功,因为在这次请求里,Filter根据刚才的认证结果,判断出来你有了权限;
  4. 退出时,需要将SecurityContextHolder里面的认证结果清理掉,这样下次请求时,Filter才能正确识别用户的权限;
  5. LoginTicketInterceptor中的afterCompletion中其实不用清理SecurityContextHolder,将这句话删掉。

2020-01-21 10:26:54

Eric.Lee 回复 牛客课程助教 : 那对于下一次请求,Security是通过用户请求中带的cookie找到SecurityContextHolder中的保存的对应用户信息和权限的吗?
2020-01-22 17:17:14

牛客课程助教 V 助教 : SecurityContextHolder 底层默认采用Session存数据, 而Session依赖于Cookie.
2020-02-10 12:14:55


SecurityConfig.java

package com.nowcoder.community.config;

import com.nowcoder.community.util.CommunityConstant;
import com.nowcoder.community.util.CommunityUtil;
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;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter implements CommunityConstant {

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/resources/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 授权
        http.authorizeRequests(以上是关于Java牛客项目课_仿牛客网讨论区_第七章的主要内容,如果未能解决你的问题,请参考以下文章

Java牛客项目课_仿牛客网讨论区_第三章

Java牛客项目课_仿牛客网讨论区_第二章

Java牛客项目课_仿牛客网讨论区_第八章

Java牛客项目课_仿牛客网讨论区_第八章

Java牛客项目课_仿牛客网讨论区_第六章

Java牛客项目课_仿牛客网讨论区_第五章