程序员最近都爱上了这个网站  程序员们快来瞅瞅吧!  it98k网:it98k.com

本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长


+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

7.1 spring security【很多知识点】

发布于2021-06-12 14:55     阅读(242)     评论(0)     点赞(27)     收藏(4)


我们已经把牛客社区功能全部实现,这一章将对项目进行一些补充完善

完善的内容包括两方面:

1.系统的安全性
2.进一步提高系统的性能
在这里插入图片描述
功能强大、方便扩展
在这里插入图片描述
认证:判断用户有无登录

如果没有登录,不可能实现 发布帖子,评论的功能。

授权: 认证之后,要判断 该用户有无访问权限,比如给帖子置顶、加精、删除,不是每个用户都能做这些事。
只有管理员、博主来可以。

servlet就是java EE。

要大致了解。spring security 底层的原理,方便之后展开学习

spring MVC的核心组件是 dispatcher Servlet,即所有请求,都要提交给dispatch Servlet进行处理,其会把请求分发给一个个控制器,由控制器 进行处理。即dispatcher Servlet 和controller是一对多的关系。
在这里插入图片描述
拦截器interceptor可以拦截访问controller的请求,这就是spring MVC底层的内容和框架。总结如下:
在这里插入图片描述

dispatcher Servlet是一个具体的Servlet接口的实现类。javaEE提出了规范——是Servlet的接口,dispatcher Servlet满足了此规范,

dispatcher Servlet即是spring MVC核心,又是java EE的规范。

java EE的规范不只是Servlet,还有一个组件——filter(过滤器),也是java EE的规范。

filter和dispatcher 之间的关系,就像拦截器和controller之间的关系。即filter可以拦截对Servlet的访问。

filter和spring MVC没有关系。

那么spring security 底层,如何利用统一的机制处理系统的各种权限呢,是利用filter组件,其底层有很多filter(大概11个),

一个filter专门验证账号密码对不对,一个filter专门做退出操作…(每个filter只做一件事)

每个filter对相应请求进行拦截,实现权限管理。

spring security 对权限控制的时机,是比较靠前的,如果没有权限,甚至不能访问dispatcher Servlet,更不必说controller
在这里插入图片描述
后面会演示,如何自定义一个filter代码以解决我们自定义的问题。

spring security 是spring 所有模块中最复杂的,该复杂不是体现在应用上,而是在使用上。

spring security 如果能吃透,那么spring 具体掌握了
http://www.spring4all.com/article/428

我们跑了一个demo

在这里插入图片描述
此外还有一个隐含路径admin,能够访问:在这里插入图片描述
现在帖子功能是有问题的,没有登录就能访问私信列表和admin
在这里插入图片描述
在这里插入图片描述
导入porn后,重启进行测试
在这里插入图片描述
可以看到首先就是要登录。

在这里插入图片描述
可以看到,在日志里有账号密码
在这里插入图片描述
登录后,可以成功访问那些功能。

需要在业务层以及数据访问层,在user实体类中
user关联角色,角色关联权限。
在这里插入图片描述
在user中实现:

    // 返回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://case为 2的情况先不考虑
                        return "ADMIN";
                    default:
                        return "USER";
                }
            }
        });
        return list;
    }

}

在UserService中:

@Service
public class UserService implements UserDetailsService {//UserDetailsService是 spring security 底层接口,检查登录的时候需要用到

    @Autowired
    private UserMapper userMapper;

    public User findUserByName(String username) {
        return userMapper.selectByName(username);
    }
//添加的部分:
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//根据用户名查用户,和上面findUserByName方法类似。只不过名字不一样,这也是该方法为啥要写在UserService里面的原因
        return this.findUserByName(username);
    }
}

在SecurityConfig包下,新建:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;//因为该组件实现了UserDetailsService 接口

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 忽略静态资源的访问
        web.ignoring().antMatchers("/resources/**");//忽视resources下所有资源,都是静态资源,因为静态资源没有什么可保密的,可以随意访问,可以提高性能。
    }

    // AuthenticationManager: 认证的核心接口.
    // AuthenticationManagerBuilder: 用于构建AuthenticationManager对象的工具类.
    // ProviderManager: AuthenticationManager接口的默认实现类.
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 内置的认证规则(权限管理是两个方面。认证+授权)
        // auth.userDetailsService(userService).passwordEncoder(new Pbkdf2PasswordEncoder("12345"));//Pbkdf2PasswordEncoder根据sault进行加密

        // 自定义认证规则,不用内置的默认规则,只不过稍微麻烦
        // AuthenticationProvider: ProviderManager持有一组(一组 即多个)AuthenticationProvider,每个AuthenticationProvider负责一种认证。ProviderManager不亲自认证,而是里面的很多小弟AuthenticationProvider,每一个负责一种认证
        // 上面即委托模式: ProviderManager将认证委托给AuthenticationProvider.
        auth.authenticationProvider(new AuthenticationProvider() {//传入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("密码不正确!");
                }

                // UsernamePasswordAuthenticationToken中 principal: 认证的主要信息,一般是user; credentials: 证书,在账号密码模式下是密码; authorities: 权限;
                return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());//返回认证结果, user.getAuthorities()当前用户的权限
            }


            // 当前的AuthenticationProvider支持哪种类型的认证.
            @Override
            public boolean supports(Class<?> aClass) {
                // UsernamePasswordAuthenticationToken: Authentication接口的常用的实现类.代表账号密码验证
                return UsernamePasswordAuthenticationToken.class.equals(aClass);
            }
        });
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //避开默认登录页面,整我们自己的登录页面——登录相关配置,告诉security我们的登录界面
        http.formLogin()
                .loginPage("/loginpage")
                .loginProcessingUrl("/login")
                .successHandler(new AuthenticationSuccessHandler() {//AuthenticationSuccessHandler成功时接口
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        response.sendRedirect(request.getContextPath() + "/index");//重定向到首页
                    }
                })
                .failureHandler(new AuthenticationFailureHandler() {//AuthenticationSuccessHandler失败时接口
                    @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");//跳转至(重定向)首页
                    }
                });

在homecontroller中

@Controller
public class HomeController {

    @RequestMapping(path = "/index", method = RequestMethod.GET)
    public String getIndexPage(Model model) {
        // 认证成功后,结果会通过SecurityContextHolder存入SecurityContext中.(通过底层的filte实现)
        Object obj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();//SecurityContextHolder.getContext()得到SecurityContext对象,其中存着认证结果即.getAuthentication(),通过getPrincipal得到主要信息
        if (obj instanceof User) {//如果确实是User,说明登录成功
            model.addAttribute("loginUser", obj);
        }
        return "/index";
    }

    @RequestMapping(path = "/discuss", method = RequestMethod.GET)
    public String getDiscussPage() {
        return "/site/discuss";
    }

    @RequestMapping(path = "/letter", method = RequestMethod.GET)
    public String getLetterPage() {
        return "/site/letter";
    }

    @RequestMapping(path = "/admin", method = RequestMethod.GET)
    public String getAdminPage() {
        return "/site/admin";
    }

    @RequestMapping(path = "/loginpage", method = {RequestMethod.GET, RequestMethod.POST})
    public String getLoginPage() {
        return "/site/login";
    }

    // 拒绝访问时的提示页面
    @RequestMapping(path = "/denied", method = RequestMethod.GET)
    public String getDeniedPage() {
        return "/error/404";
    }

}

}

重定向

浏览器访问 服务器中的A组件,A组件没有结果返回。例如,访问删除,无需返回。但是需要给浏览器显示一些内容,
例如B组件是查询,删除以后要返回查询页面,两个独立组件之间的跳转,适合用重定向。

下图这样a直接调用b,容易产生耦合,而重定向 可以降低耦合度。
在这里插入图片描述
重定向:
在这里插入图片描述
两个组件之间若想共享数据,只能通过 cookie,session

还有一种,A只能处理请求的一半,剩下一半需要b处理,由b处理剩下的,并返回服务器,这样a和b可以通过request进行数据共享(整个过程是一个请求(来自浏览器),a给b的时候,要把request和response给b)
在这里插入图片描述
在login.html中

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>

    <h1>登录社区</h1>

    <form method="post" th:action="@{/login}"><!-- 表单的路径 -->
        <p style="color:red;" th:text="${error}"><!-- 点击登录认证没通过时,还要回到该页面,且要给出错误提示 -->
            <!--提示信息-->
        </p>
        <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>

</body>
</html>

在index.html中

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>

    <h1>社区首页</h1>
    <!--欢迎信息-->
    <p th:if="${loginUser!=null}">
        欢迎你, <span th:text="${loginUser.username}"></span>!
    </p>

    <ul>
        <li><a th:href="@{/discuss}">帖子详情</a></li>
        <li><a th:href="@{/letter}">私信列表</a></li>
        <li><a th:href="@{/loginpage}">登录</a></li>
        <!--<li><a th:href="@{/loginpage}">退出</a></li>-->
        <li>
            <form method="post" th:action="@{/logout}"><!--action是路径-->
                <a href="javascript:document.forms[0].submit();">退出</a> <!--退出必须是post请求,而不是get,不符合要求-->  <!--退出是提交表单,所以不方便写链接,写一段简单的js-->
            </form>
        </li>
    </ul>

</body>
</html>

测试:
在这里插入图片描述
在这里插入图片描述
登录成功:
在这里插入图片描述
访问不了admin
在这里插入图片描述
退出以后 访问不了私信
在这里插入图片描述
验证码、记住我两个功能的实现:

验证码是在账号密码之前验证的,验证码都不对的话,无需验证账号密码。
因此在账号密码验证之前,增加一个filter

        // 增加Filter,处理验证码
        http.addFilterBefore(new Filter() {//第一个参数new Filter()表示实例化一个Filter,第二个参数表示在哪个Filter之前新加Filter。第137行
            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                HttpServletRequest request = (HttpServletRequest) servletRequest;//servletRequest是HttpServletRequest的父接口,向下转型必须强制转型
                HttpServletResponse response = (HttpServletResponse) servletResponse;
                if (request.getServletPath().equals("/login")) {//判断是否为登录请求,只有登录请求才需要处理验证码逻辑
                    String verifyCode = request.getParameter("verifyCode");
                    if (verifyCode == null || !verifyCode.equalsIgnoreCase("1234")) {//验证码为空或者不为1234
                        request.setAttribute("error", "验证码错误!");
                        request.getRequestDispatcher("/loginpage").forward(request, response);//验证码错误 需要回到登录页面
                        return;
                    }
                }
                // 让请求继续向下执行.走到下一个Filter,如果下面没有Filter,则走到servlet。//如果没有这句话,请求到此终止,不会在往下走
                filterChain.doFilter(request, response);
            }
        }, UsernamePasswordAuthenticationFilter.class);//在此Filter之前新加Filter

    }
}
// 记住我
http.rememberMe()
        .tokenRepository(new InMemoryTokenRepositoryImpl())//默认接口
        .tokenValiditySeconds(3600 * 24)//过期时间,单位s,24h,过了这个时间就不记住了
        .userDetailsService(userService);//要记住的信息userDetailsService,靠此信息通过凭证。

在login.html中

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>

    <h1>登录社区</h1>

    <form method="post" th:action="@{/login}"><!-- 表单的路径 -->
        <p style="color:red;" th:text="${error}"><!-- 点击登录认证没通过时,还要回到该页面,且要给出错误提示 -->
            <!--提示信息-->
        </p>
        <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"> 记住我<!-- checkbox 表示勾选框 -->
        </p>
        <p>
            <input type="submit" value="登录">
        </p>
    </form>

</body>
</html>

测试:
在这里插入图片描述
在这里插入图片描述
当选中“记住我”功能后
在这里插入图片描述
当我们关掉浏览器,再次打开网站后,不需要登录。帮我们绕过了认证的过程。
在这里插入图片描述



所属网站分类: 技术文章 > 博客

作者:忽明忽灭

链接:http://www.javaheidong.com/blog/article/222520/d7563039562450c4cf1b/

来源:java黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

27 0
收藏该文
已收藏

评论内容:(最多支持255个字符)