尚筹网 —— 9、权限控制(加入 Spring-Security)

目录

1、加入 Spring-Security 环境

1.1、加入依赖

1.2、在 web.xml 中配置 DelegatingFilterProxy

1.3、创建基于注解的配置类

1.4、谁来把 WebAppSecurityConfig 扫描到 IOC 里?

1.5、找不到 bean 的问题分析

1.5.1、明确三大组件启动顺序

1.5.2、DelegatingFilterProxy 查找 IOC 容器然后查找 bean 的工作机制

1.5.3、解决方法一:改源码

1.5.4、解决方法二:把两个 IOC 容器合二为一

2、目标

2.1、放行登录页和静态资源

2.2、提交登录表单做内存认证

2.3、退出登录

2.4、把内存登录改成数据库登录

2.5、密码加密

2.6、页面显示用户昵称

2.10、SpringSecurity 登陆后密码擦除

2.11、权限控制

2.11.1、设置测试数据

2.11.2、测试一:访问 Admin 分页时具备“经理”角色

2.11.3、测试二:访问 Role 的分页时具备“部长”角色

2.11.4、测试三:要求:访问 Admin 分页功能时具备“经理”角色或“user:get”权限二者之一

2.11.5、测试四:访问 Admin 保存功能时具备 user:save 权限

2.12、页面元素的权限控制


1、加入 Spring-Security 环境

1.1、加入依赖

在 atcrowdfunding01-admin-parent 模块和 atcrowdfunding05-common-util 模块的 pom.xml 加入依赖



	org.springframework.security
	spring-security-web
	4.2.10.RELEASE



	org.springframework.security
	spring-security-config
	4.2.10.RELEASE



	org.springframework.security
	spring-security-taglibs
	4.2.10.RELEASE

1.2、在 web.xml 中配置 DelegatingFilterProxy

    
    
        
        springSecurityFilterChain
        org.springframework.web.filter.DelegatingFilterProxy
    
    
        springSecurityFilterChain
        /*
    

1.3、创建基于注解的配置类

尚筹网 —— 9、权限控制(加入 Spring-Security)_第1张图片

package com.atguigu.crowd.mvc.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * @Author zhang
 * @Date 2022/6/6 - 16:18
 * @Version 1.0
 */
@Configuration  // 表示当前类是一个配置类
@EnableWebSecurity  // 启动Web环境下权限控制功能
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {

}

1.4、谁来把 WebAppSecurityConfig 扫描到 IOC 里?

如果是 Spring 的 IOC 容器扫描:

尚筹网 —— 9、权限控制(加入 Spring-Security)_第2张图片

如果是 SpringMVC 的 IOC 容器扫描:

尚筹网 —— 9、权限控制(加入 Spring-Security)_第3张图片

结论:为了让 SpringSecurity 能够针对浏览器请求进行权限控制,需要让 SpringMVC 来扫描 WebAppSecurityConfig 类。

衍生问题:DelegatingFilterProxy 初始化时需要到 IOC 容器查找一个 bean, 这个 bean 所在的 IOC 容器要看是谁扫描了 WebAppSecurityConfig。

如果是 Spring 扫描了 WebAppSecurityConfig,那么 Filter 需要的 bean 就在 Spring 的 IOC 容器。

如果是 SpringMVC 扫描了 WebAppSecurityConfig,那么 Filter 需要的 bean 就在 SpringMVC 的 IOC 容器。

1.5、找不到 bean 的问题分析

06-Jun-2022 17:25:51.237 严重 [RMI TCP Connection(3)-127.0.0.1] org.apache.catalina.core.StandardContext.filterStart 启动过滤器异常
	org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'springSecurityFilterChain' available

1.5.1、明确三大组件启动顺序

首先:ContextLoaderListener 初始化,创建 Spring 的 IOC 容器

其次:DelegatingFilterProxy 初始化,查找 IOC 容器、查找 bean

最后:DispatcherServlet 初始化,创建 SpringMVC 的 IOC 容器

1.5.2、DelegatingFilterProxy 查找 IOC 容器然后查找 bean 的工作机制

尚筹网 —— 9、权限控制(加入 Spring-Security)_第4张图片

1.5.3、解决方法一:改源码

① 创建与 DelegatingFilterProxy 类相同的包,在包下创建 DelegatingFilterProxy 类,其内容与源码的一样

尚筹网 —— 9、权限控制(加入 Spring-Security)_第5张图片

② 设置初始化时直接跳过查找 IOC 容器的环节

将下图红框代码注释

尚筹网 —— 9、权限控制(加入 Spring-Security)_第6张图片

③ 第一次请求时直接找 SpringMVC 的 IOC 容器

修改 doFilter 方法,位置如下图

尚筹网 —— 9、权限控制(加入 Spring-Security)_第7张图片

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        // Lazily initialize the delegate if necessary.
        Filter delegateToUse = this.delegate;
        if (delegateToUse == null) {
            synchronized (this.delegateMonitor) {
                delegateToUse = this.delegate;
                if (delegateToUse == null) {

                    // 把原来查找IOC容器的代码注释
                    // WebApplicationContext wac = findWebApplicationContext();

                    // 按我们自己的需要重新编写
                    // 获取 ServletContext 对象
                    ServletContext sc = this.getServletContext();

                    // 拼接 SpringMVC 将 IOC 容器存入 ServletContext 域的时候使用的属性名
                    String servletName = "springDispatcherServlet";
                    String attrName = FrameworkServlet.SERVLET_CONTEXT_PREFIX + servletName;

                    // 根据 attrName 从 ServletContext 域中获取 IOC 容器对象
                    WebApplicationContext wac = (WebApplicationContext) sc.getAttribute(attrName);

                    if (wac == null) {
                        throw new IllegalStateException("No WebApplicationContext found: " +
                                "no ContextLoaderListener or DispatcherServlet registered?");
                    }
                    delegateToUse = initDelegate(wac);
                }
                this.delegate = delegateToUse;
            }
        }

        // Let the delegate perform the actual doFilter operation.
        invokeDelegate(delegateToUse, request, response, filterChain);
    }

1.5.4、解决方法二:把两个 IOC 容器合二为一

不使用 ContextLoaderListener,让 DispatcherServlet 加载所有 Spring 配置文件。

方法:取消 web.xml  中配置 ContextLoaderListener 并加载所有 spring 的配置文件

尚筹网 —— 9、权限控制(加入 Spring-Security)_第8张图片

尚筹网 —— 9、权限控制(加入 Spring-Security)_第9张图片

缺点:会破坏现有程序的结构。原本是 ContextLoaderListener 和 DispatcherServlet 两个组件创建两个 IOC 容器,现在改成只有一个。

2、目标

2.1、放行登录页和静态资源

在 SpringSecurity 的配置类中配置

@Configuration  // 表示当前类是一个配置类
@EnableWebSecurity  // 启动Web环境下权限控制功能
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()    // 对请求进行授权
                .antMatchers("/admin/to/login/page.html","/bootstrap/**","/script/**","/ztree/**",
                        "/crowd/**","/css/**","/fonts/**","/img/**","/jquery/**","/layer/**")   // 对登录页及静态资源设置
                .permitAll()    // 无条件访问
                .anyRequest()   // 对所有请求设置
                .authenticated()    // 需要认证
                ;
    }
}

2.2、提交登录表单做内存认证

尚筹网 —— 9、权限控制(加入 Spring-Security)_第10张图片

① 设置 admin-login.jsp 页面的表单,修改表单提交的地址为 action="security/do/login.html",添加错误信息显示

尚筹网 —— 9、权限控制(加入 Spring-Security)_第11张图片

action="security/do/login.html"

${SPRING_SECURITY_LAST_EXCEPTION.message }

 ② 将之前在 spring-web-mvc.xml 的拦截器注释

尚筹网 —— 9、权限控制(加入 Spring-Security)_第12张图片

③ SpringSecurity 配置

在 configure(HttpSecurity http) 进行设置

                .csrf()     // 防跨站请求伪造功能
                .disable()  // 禁用csrf
                .formLogin()    // 开启表单登录功能
                .loginPage("/admin/to/login/page.html") // 指定登录页
                .loginProcessingUrl("/security/do/login.html")  // 指定处理登录请求的地址
                .defaultSuccessUrl("/admin/to/main/page.html")  // 指定登录成功后前往的地址
                .usernameParameter("loginAcct") // 表单中账号的请求参数名
                .passwordParameter("userPswd")  // 表单中密码的请求参数名

在 configure(AuthenticationManagerBuilder auth) 设置

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                .withUser("tom")
                .password("123123")
                .roles("ADMIN")
                ;
    }

2.3、退出登录

① 在 include-nav.jsp 修改退出登录的超链接

尚筹网 —— 9、权限控制(加入 Spring-Security)_第13张图片

 ② 在 SpringSecurity 的配置类的 configure(HttpSecurity http) 方法设置

                .logout()   // 开启退出登录功能
                .logoutUrl("/security/do/logout.html")    // 指定退出登录地址
                .logoutSuccessUrl("/admin/to/login/page.html")     // 指定退出成功后前往的地址

2.4、把内存登录改成数据库登录

尚筹网 —— 9、权限控制(加入 Spring-Security)_第14张图片

① 根据 adminId 查询已分配的角色,这个操作在之前已经写好

② 根据 adminId 查询已分配权限

AuthMapper.xml

  

AuthMapper

    /**
     * 根据adminId查询已分配的权限
     * @param adminId
     * @return
     */
    List selectAssignedAuthNameByAdminId(Integer adminId);

AuthService

    /**
     * 根据adminId查询已分配的权限
     * @param adminId
     * @return
     */
    List getAssignedAuthNameByAdminId(Integer adminId);

AuthServiceImpl

    /**
     * 根据adminId查询已分配的权限
     * @param adminId
     * @return
     */
    @Override
    public List getAssignedAuthNameByAdminId(Integer adminId) {
        return authMapper.selectAssignedAuthNameByAdminId(adminId);
    }

③ 创建 SecurityAdmin 类

尚筹网 —— 9、权限控制(加入 Spring-Security)_第15张图片

package com.atguigu.crowd.mvc.config;

import com.atguigu.crowd.entity.Admin;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.List;

/**
 * @Author zhang
 * @Date 2022/6/8 - 19:27
 * @Version 1.0
 */


// 考虑到User对象中仅仅包含账号和密码,为了能够获取到原始的Admin对象,专门创建这个类对User类进行扩展
public class SecurityAdmin extends User {

    private static final long serialVersionUID = 1L;

    // 原始的Admin对象,包含Admin对象的全部属性
    private Admin originalAdmin;

    /**
     * 构造器
     * @param originalAdmin 原始的Admin对象
     * @param authorities 该对象的角色、权限信息的集合
     */
    public SecurityAdmin(Admin originalAdmin, List authorities){
        // 调用父类构造器
        super(originalAdmin.getLoginAcct(), originalAdmin.getUserPswd(), authorities);
        this.originalAdmin = originalAdmin;
    }

    public Admin getOriginalAdmin() {
        return originalAdmin;
    }
}

④ 根据账号查询 Admin

AdminService

    /**
     * 根据账号查询admin
     * @param username
     * @return
     */
    Admin getAdminByLoginAcct(String username);

AdminServiceImpl

    /**
     * 根据账号查询admin
     * @param username
     * @return
     */
    @Override
    public Admin getAdminByLoginAcct(String username) {
        AdminExample adminExample = new AdminExample();
        AdminExample.Criteria criteria = adminExample.createCriteria();
        criteria.andLoginAcctEqualTo(username);
        List admins = adminMapper.selectByExample(adminExample);
        Admin admin = admins.get(0);
        return admin;
    }

⑤ 创建 UserDetailsService 实现类

尚筹网 —— 9、权限控制(加入 Spring-Security)_第16张图片

package com.atguigu.crowd.mvc.config;

import com.atguigu.crowd.entity.Admin;
import com.atguigu.crowd.entity.Role;
import com.atguigu.crowd.service.api.AdminService;
import com.atguigu.crowd.service.api.AuthService;
import com.atguigu.crowd.service.api.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author zhang
 * @Date 2022/6/8 - 19:35
 * @Version 1.0
 */
@Component
public class CrowdUserDetailsService implements UserDetailsService {

    @Autowired
    private AdminService adminService;

    @Autowired
    private RoleService roleService;

    @Autowired
    private AuthService authService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根据账号名称查询Admin对象
        Admin admin = adminService.getAdminByLoginAcct(username);

        // 获取adminId
        Integer adminId = admin.getId();

        // 根据adminId查询角色信息
        List assignedRoleList = roleService.getAssignedRole(adminId);

        // 根据adminId查询权限信息
        List authNameList = authService.getAssignedAuthNameByAdminId(adminId);

        // 存入角色和权限信息
        List authorities = new ArrayList<>();
        for (Role role : assignedRoleList) {
            String roleName = "ROLE_" + role.getName();
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(roleName);
            authorities.add(simpleGrantedAuthority);
        }
        for (String authName : authNameList) {
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authName);
            authorities.add(simpleGrantedAuthority);
        }

        // 封装
        SecurityAdmin securityAdmin = new SecurityAdmin(admin, authorities);
        
        return securityAdmin;
    }
}

⑥ 在配置类中使用 UserDetailsService

先注入 UserDetailsService,再在 configure(AuthenticationManagerBuilder auth) 方法使用

    @Autowired
    private UserDetailsService userDetailsService;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用内存登录
        //auth
        //        .inMemoryAuthentication()
        //        .withUser("tom")
        //        .password("123123")
        //        .roles("ADMIN")

        // 使用数据库登录
        auth
                .userDetailsService(userDetailsService)
                ;
    }

2.5、密码加密

① 修改 t_admin 表结构

由于使用带盐值的加密方式,生成的密文长度超过之前定义的长度,所以要修改数据表的密码长度

ALTER TABLE `project_crowd`.`t_admin` CHANGE `user_pswd` `user_pswd` CHAR(100) CHARSET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '登录密码 '; 

② 使用 BCryptPasswordEncoder 对象

一、若在加入 SpringSecurity 时使用改源码的方式,在 spring-persist-tx.xml 中配置




在 WebAppSecurityConfig 中配置

@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
        auth
                .userDetailsService(userDetailsService)
                .passwordEncoder(bCryptPasswordEncoder())
                ;

二、若在加入 SpringSecurity 时使用将 容器 合并的方式,在 WebAppSecurityConfig 配置即可

    @Bean
    public BCryptPasswordEncoder getBCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
        auth
                .userDetailsService(userDetailsService)
                .passwordEncoder(getBCryptPasswordEncoder())
                ;

2.6、页面显示用户昵称

显示用户昵称的页面为 include-nav.jsp,以下在该页面进行修改

① 导入SpringSecurity的标签库

<%-- 导入SpringSecurity标签库 --%>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>

② 通过标签获取登录的用户昵称


	<%--${sessionScope.loginAdmin.userName}--%>
	

SpringSecurity 处理完登录操作之后把登录成功的 User 对象以 principal 属性名存入了 UsernamePasswordAuthenticationToken 对象

Principal:
访问 SecurityAdmin 对象的属性:
访问 SecurityAdmin 对象的属性:
访问 SecurityAdmin 对象的属性:
访问 SecurityAdmin 对象的属性:
访问 SecurityAdmin 对象的属性:

2.10、SpringSecurity 登陆后密码擦除

擦除密码是在不影响登录认证的情况下,避免密码泄露,增强系统的安全性

本身 SpringSecurity 是会自动把 User 对象中的密码部分擦除。

尚筹网 —— 9、权限控制(加入 Spring-Security)_第17张图片

但是我们创建 SecurityAdmin 对象扩展了 User 对象,User 对象中的密码被擦除了,但是原始 Admin 对象中的密码没有擦除。如果要把原始的 Admin 对象中的密码也擦除需要修改 SecurityAdmin 类代码:

尚筹网 —— 9、权限控制(加入 Spring-Security)_第18张图片

    /**
     * 构造器
     * @param originalAdmin 原始的Admin对象
     * @param authorities 该对象的角色、权限信息的集合
     */
    public SecurityAdmin(Admin originalAdmin, List authorities){
        // 调用父类构造器
        super(originalAdmin.getLoginAcct(), originalAdmin.getUserPswd(), authorities);

        // 给本类的originalAdmin赋值
        this.originalAdmin = originalAdmin;

        // 将原始的Admin对象中的密码擦除
        this.originalAdmin.setUserPswd(null);
    }

2.11、权限控制

2.11.1、设置测试数据

尚筹网 —— 9、权限控制(加入 Spring-Security)_第19张图片

t_admin 表
尚筹网 —— 9、权限控制(加入 Spring-Security)_第20张图片 t_role 表
尚筹网 —— 9、权限控制(加入 Spring-Security)_第21张图片 t_auth 表
尚筹网 —— 9、权限控制(加入 Spring-Security)_第22张图片 inner_role_auth 表
尚筹网 —— 9、权限控制(加入 Spring-Security)_第23张图片 inner_admin_role 表

2.11.2、测试一:访问 Admin 分页时具备“经理”角色

在 WebAppSecurityConfig 中的 configure(HttpSecurity http) 方法设置

                .antMatchers("/admin/get/page.html")    // 访问 Admin 分页功能时要求具备“经理”角色
                .hasRole("经理")

2.11.3、测试二:访问 Role 的分页时具备“部长”角色

在 WebAppSecurityConfig 加上 @EnableGlobalMethodSecurity,启动全局方法权限控制

// 启动全局方法权限控制,并且设置 prePostEnabled = true,保证@PreAuthority、@PostAuthority、@PostFilter生效
@EnableGlobalMethodSecurity(prePostEnabled = true)

在 RoleHandle 中的 getPageInfo 方法加上注解 @PreAuthorize

@PreAuthorize("hasRole('部长')")

尚筹网 —— 9、权限控制(加入 Spring-Security)_第24张图片

修改异常处理器 CrowdExceptionResolver 中跳转到 system-error 的方法

    /**
     * 修改帐号已有的异常映射
     * @param exception
     * @param request
     * @param response
     * @return
     * @throws IOException
     */
    //@ExceptionHandler(value = LoginAcctAlreadyInUseForUpdateException.class)
    //public ModelAndView resolvLoginAcctAlreadyInUseForUpdateException(LoginAcctAlreadyInUseForUpdateException exception, HttpServletRequest request, HttpServletResponse response) throws IOException {
    //    String viewName = "system-error";
    //    return commonResolve(viewName, exception, request, response);
    //}
    @ExceptionHandler(value = Exception.class)
    public ModelAndView resolveException(Exception exception, HttpServletRequest request, HttpServletResponse response) throws IOException {
        String viewName = "system-error";
        return commonResolve(viewName, exception, request, response);
    }

在 WebAppSecurityConfig 中的 configure(HttpSecurity http) 方法配置异常页面的提示信息

.exceptionHandling()
.accessDeniedHandler(new AccessDeniedHandler() {
	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response,
					   AccessDeniedException accessDeniedException) throws IOException, ServletException {
		request.setAttribute("exception",new Exception(CrowdConstant.MESSAGE_ACCESS_DENIED));
		request.getRequestDispatcher("/WEB-INF/system-error.jsp").forward(request, response);
	}
})

2.11.4、测试三:要求:访问 Admin 分页功能时具备“经理”角色或“user:get”权限二者之一

添加查询权限给部长操作者角色

尚筹网 —— 9、权限控制(加入 Spring-Security)_第25张图片 t_auth 表
尚筹网 —— 9、权限控制(加入 Spring-Security)_第26张图片 inner_role_auth 表

在 WebAppSecurityConfig 中设置

.antMatchers("/admin/get/page.html")    // 访问 Admin 分页功能时要求具备“经理”角色
//.hasRole("经理")
.access("hasRole('经理') OR hasAuthority('user:get')")

2.11.5、测试四:访问 Admin 保存功能时具备 user:save 权限

在 AdminHandle 的 save 方法加上注解

@PreAuthorize("hasAuthority('user:save')")

尚筹网 —— 9、权限控制(加入 Spring-Security)_第27张图片

2.12、页面元素的权限控制

页面上的局部元素根据访问控制规则进行控制。

尚筹网 —— 9、权限控制(加入 Spring-Security)_第28张图片


你可能感兴趣的:(尚硅谷尚筹网学习笔记,大数据)