尚筹网 —— 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 加入依赖

<!-- SpringSecurity 对 Web 应用进行权限管理 -->
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-web</artifactId>
	<version>4.2.10.RELEASE</version>
</dependency>
<!-- SpringSecurity 配置 -->
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-config</artifactId>
	<version>4.2.10.RELEASE</version>
</dependency>
<!-- SpringSecurity 标签库 -->
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-taglibs</artifactId>
	<version>4.2.10.RELEASE</version>
</dependency>

1.2、在 web.xml 中配置 DelegatingFilterProxy

    <!--
    注意:SpringSecurity 会根据 DelegatingFilterProxy 的 filter-name 到 IOC 容器中
    查找所需要的 bean。所以 filter-name 必须是 springSecurityFilterChain 名字。
    -->
    <filter>
        <!-- filter-name 标签必须为springSecurityFilterChain -->
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

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

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 容器扫描:

如果是 SpringMVC 的 IOC 容器扫描:

结论:为了让 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 的工作机制

1.5.3、解决方法一:改源码

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

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

将下图红框代码注释

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

修改 doFilter 方法,位置如下图

    @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 的配置文件

缺点:会破坏现有程序的结构。原本是 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、提交登录表单做内存认证

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

action="security/do/login.html"

<p>${SPRING_SECURITY_LAST_EXCEPTION.message }</p>

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

③ 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 修改退出登录的超链接

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

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

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

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

② 根据 adminId 查询已分配权限

AuthMapper.xml

  <select id="selectAssignedAuthNameByAdminId" resultType="java.lang.String">
    SELECT DISTINCT `t_auth`.`name`
    FROM `t_auth`
           LEFT JOIN `inner_role_auth` ON `t_auth`.`id` = `inner_role_auth`.`auth_id`
           LEFT JOIN `inner_admin_role` ON `inner_admin_role`.`role_id` = `inner_role_auth`.`role_id`
    WHERE `inner_admin_role`.`admin_id` = #{adminId} AND `t_auth`.`name` != "" AND `t_auth`.`name` IS NOT NULL
  </select>

AuthMapper

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

AuthService

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

AuthServiceImpl

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

③ 创建 SecurityAdmin 类

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<GrantedAuthority> 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<Admin> admins = adminMapper.selectByExample(adminExample);
        Admin admin = admins.get(0);
        return admin;
    }

⑤ 创建 UserDetailsService 实现类

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<Role> assignedRoleList = roleService.getAssignedRole(adminId);

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

        // 存入角色和权限信息
        List<GrantedAuthority> 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 中配置

<!--  配置 BCryptPasswordEncoder,不在WebAppSecurityConfig中使用@bean的原因
     是:在那个类中配置的bean加入到的是SpringMVC的Ioc容器中,而使用时的xxxServiceImpl中
     是从Spring的IOC容器中取,因此拿不到bean
-->
<bean id="BCryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder">
</bean>

在 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" %>

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

<i class="glyphicon glyphicon-user"></i>
	<%--${sessionScope.loginAdmin.userName}--%>
	<security:authentication property="username"/>
<span class="caret"></span>
<p>SpringSecurity 处理完登录操作之后把登录成功的 User 对象以 principal 属性名存入了 UsernamePasswordAuthenticationToken 对象</p>
Principal:<security:authentication property="principal.class.name"/><br/>
访问 SecurityAdmin 对象的属性:<security:authentication property="principal.originalAdmin.loginAcct"/><br/>
访问 SecurityAdmin 对象的属性:<security:authentication property="principal.originalAdmin.userPswd"/><br/>
访问 SecurityAdmin 对象的属性:<security:authentication property="principal.originalAdmin.userName"/><br/>
访问 SecurityAdmin 对象的属性:<security:authentic ation property="principal.originalAdmin.email"/><br/>
访问 SecurityAdmin 对象的属性:<security:authentication property="principal.originalAdmin.createTime"/><br/>

2.10、SpringSecurity 登陆后密码擦除

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

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

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

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

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

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

2.11、权限控制

2.11.1、设置测试数据

t_admin 表
t_role 表
t_auth 表
inner_role_auth 表
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('部长')")

修改异常处理器 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”权限二者之一

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

t_auth 表
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')")

2.12、页面元素的权限控制

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

<security:authorize access="hasRole('经理')"></security:authorize>
<security:authorize access="hasAuthority('role:delete')"></security:authorize>

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值