SpringSecurity 在 SSM架构中的使用

SpringSecurity - SSM

SpringSecurity 对比 Shiro

SpringSecurity的特点:

  • 能和 Spring无缝贴合
  • 能实现全面的权限控制
  • 专门为 Web开发而设计
    • 旧版本不能脱离 Web环境使用
    • 新版本单独引入核心模块可以脱离 Web环境使用
  • 重量级

Shiro的特点:

  • 轻量级
  • 通用性
  • 但是在 Web环境下某些特定的需求需要手动编写代码定制

首先搭建 Web环境

引入 Web依赖

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>4.3.20.RELEASE</version>
    </dependency>
    <!-- 引入 Servlet 容器中相关依赖 -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>servlet-api</artifactId>
        <version>2.5</version>
        <scope>provided</scope>
    </dependency>
    <!-- JSP 页面使用的依赖 -->
    <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>jsp-api</artifactId>
        <version>2.1.3-b06</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

创建 SpringMVC的配置文件

<context:component-scan
                        base-package="com.atguigu.security"></context:component-scan>
<bean
      class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/views/"></property>
    <property name="suffix" value=".jsp"></property>
</bean>
<mvc:annotation-driven></mvc:annotation-driven>
<mvc:default-servlet-handler />

修改 web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <servlet>
        <servlet-name>springDispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springDispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

逐步深入 SpringSecurity

加入 SpringSecurity的依赖

<!-- 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>

加入 SpringSecurity权限控制的 Filter

  • SpringSecurity 使用的是过滤器 Filter而不是拦截器 Interceptor
    • 意味着 SpringSecurity 能够管理的不仅仅是 SpringMVC 中的 controller请求,还包含 Web 应用中所有请求。
    • 比如:项目中的静态资源也会被拦截,从而进行权限控制。

修改 web.xml

<filter>
    <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>

注意: <filter-name>springSecurityFilterChain</filter-name> 标 签 中 必 须 是 springSecurityFilterChain

加入 WebSecurity的配置类

@Configuration
@EnableWebSecurity // 开启 SpringSecurity关于 Web项目的注解支持
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
    
}

实验 1:放行首页和静态资源

在配置类中重写父类的 configure(HttpSecurity security)方法

protected void configure(HttpSecurity security) throws Exception {
    logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially
                 override subclass configure(HttpSecurity).");
                 security
                 .authorizeRequests()
                 .anyRequest().authenticated() //所有请求都需要进行认证
                 .and()
                 .formLogin()
                 .and()
                 .httpBasic();
                 }

重写后:

@Override
protected void configure(HttpSecurity security) throws Exception {
    //super.configure(security); 注释掉将取消父类方法中的默认规则
    security.authorizeRequests() //对请求进行授权
        .antMatchers("/layui/**","/index.jsp") //使用 ANT 风格设置要授权的 URL 地址
        .permitAll() //允许上面使用 ANT 风格设置的全部请求
        .anyRequest() //其他未设置的全部请求
        .authenticated(); //需要认证
}

实验 2:未认证请求跳转到登录页

@Override
protected void configure(HttpSecurity security) throws Exception {
    //super.configure(security); 注释掉将取消父类方法中的默认规则
    security.authorizeRequests() //对请求进行授权
        .antMatchers("/layui/**","/index.jsp") //使用 ANT 风格设置要授权的 URL 地
        址
        .permitAll() //允许上面使用 ANT 风格设置的全部请求
        .anyRequest() //其他未设置的全部请求
        .authenticated() //需要认证
        .and()
        .formLogin() //设置未授权请求跳转到登录页面
        .loginPage("/index.jsp") //指定登录页
        .permitAll() //为登录页设置所有人都可以访问
        // loginProcessingUrl()方法指定了登录地址,就会覆盖 loginPage()方法中设置的默认值 index.jsp POST
        .loginProcessingUrl("/do/login.html") // 指定提交登录表单的地址
        ;
}

实验 3:设置登录系统的账号、密码

SpringSecurity 默认账号的请求参数名:username

SpringSecurity 默认密码的请求参数名:password

为了让 SpringSecurity接管用户登陆,要么修改页面上的表单项的 name 属性值,要么修改配置。

可以通过如下方法修改配置:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .formLogin()
        .usernameParameter("xxxName") // 定制登录账号的请求参数名
        .passwordParameter("xxxPswd") // 定制登录密码的请求参数名
        .defaultSuccessUrl("/main.html"); //设置登录成功后默认前往的 URL 地址
        ;
}

重写另外一个父类的方法,来设置登录系统的账号密码

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //super.configure(auth); 一定要禁用默认规则
    auth
        .inMemoryAuthentication()
        .withUser("tom").password("123123") //设置账号密码
        .roles("ADMIN") //设置角色
        .and()
        .withUser("jerry").password("456456")//设置另一个账号密码
        .authorities("SAVE","EDIT"); //设置权限
}

Cannot pass a null GrantedAuthority collection 问 题 是 由 于 没 有 设 置 roles() 或 authorities()方法导致的

注意:登陆时需要携带参数 <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

当然,也可以禁用 csrf

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .csrf()
        .disable() // 禁用 csrf
        ;
}

实验 4:用户注销

通过调用 HttpSecurity 对象的一系列方法设置注销功能。

  • logout()方法:开启注销功能
  • logoutUrl()方法:自定义注销功能的 URL 地址
  • logoutSuccessUrl()方法:退出成功后前往的 URL 地址
  • addLogoutHandler()方法:添加退出处理器
  • logoutSuccessHandler()方法:退出成功处理器

如果 CSRF 功能没有禁用,那么退出请求必须是 POST 方式。如果禁用了 CSRF功能则任何请求方式都可以。

实验 5:基于角色或权限进行访问控制

通过 HttpSecurity 对象设置资源的角色要求

protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests() //对请求进行授权
        .antMatchers("/layui/**","/index.jsp") //使用 ANT 风格设置要授权的 URL 地址
        .permitAll() //允许上面使用 ANT 风格设置的全部请求
        .antMatchers("/level1/**")
        .hasRole("学徒")
        .antMatchers("/level2/**")
        .hasRole(" 大师")
        .antMatchers("/level3/**")
        .hasRole(" 宗师")
        .anyRequest() //其他未设置的全部请求
        .authenticated() //需要认证
        .and()
        .formLogin() //设置未授权请求跳转到登录页面:开启表单登录功
        能
        .loginPage("/index.jsp") //指定登录页
        .permitAll() //为登录页设置所有人都可以访问
        .defaultSuccessUrl("/main.html") //设置登录成功后默认前往的 URL 地址
        .and()
        .logout()
        .logoutUrl("/my/logout")
        .logoutSuccessUrl("/index.jsp")
        ;
}

protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
        .withUser("tom").password("123123") //设置账号密码
        .roles("ADMIN","学徒","宗师") //设置角色
        .and()
        .withUser("jerry").password("456456")//设置另一个账号密码
        .authorities("SAVE","EDIT"); //设置权限
}

类似异常的 catch顺序,路径的声明先后也很重要,大的路径声明在后面,小的在前面

实验 6 :自定义 403 错误页面

protected void configure(HttpSecurity http) throws Exception {
    http
        .exceptionHandling()
        .accessDeniedPage("/to/no/auth/page.html") // 当出现访问被拒绝时,指定跳转的页面
        .accessDeniedHandler(new AccessDeniedHandler() { // 自定义的访问拒绝策略,开启该方法后,前一个方法就会失效了
            @Override
            public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
                httpServletRequest.setAttribute("message", "抱歉!您无法访问这个资源");
                httpServletRequest.getRequestDispatcher("/WEB-INF/views/no_auth.jsp").forward(httpServletRequest, httpServletResponse);
            }
        })
        ;
}

实验 7 :记住我-内存版

登录表单携带名为 remember-me 的请求参数

如果不能使用 remember-me 作为请求参数名称,可以使用 rememberMeParameter()方法定制

protected void configure(HttpSecurity http) throws Exception {
    http
        .remeberMe(); // 开启记住我的功能
}

通过开发者工具看到浏览器端存储了名为remember-me的Cookie。根据这个Cookie 的 value 在服务器端找到以前登录的 User。

而且这个 Cookie 被设置为存储 2 个星期的时间。

实验 8 :记住我-数据库版

为了让服务器重启也不影响记住登录状态,将用户登录状态信息存入数据库。

需要在 WebAppSecurityConfig类中注入数据源

@Autowired
private DataSource dataSource;
@Configuration
@EnableWebSecurity // 启用 Web安全处理功能
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    protected void configure(HttpSecurity http) throws Exception {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        repository.setDataSource(dataSource);
        
        http
            .rememberMe()
            .tokenRepository(tokenRepository);
    }
}

注意:需要进入 JdbcTokenRepositoryImpl 类中找到创建 persistent_logins 表的 SQL 语句创建persistent_logins 表。

CREATE TABLE persistent_logins (
    username VARCHAR (64) NOT NULL,
    series VARCHAR (64) PRIMARY KEY,
    token VARCHAR (64) NOT NULL,
    last_used TIMESTAMP NOT NULL
);

实验 9:查询数据库完成认证

创建存储用户的表

create table t_admin (
    id int not null auto_increment,
    loginacct varchar(255) not null,
    userpswd char(32) not null,
    username varchar(255) not null,
    email varchar(255) not null,
    createtime char(19),
    primary key (id)
);

需要实现 UserDetailsService接口,重写 loadUserByUsername()方法加载用户

package com.atguigu.security.service;

/**
 * @author :OliQ
 * @date :Created on 2021/8/27 15:50
 */
@Service
public class MyUserDetailService implements UserDetailsService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public UserDetails loadUserByUsername(
            // 表单提交的用户名
            String s) throws UsernameNotFoundException {

        String sql = "SELECT id, loginacct, userpswd, username, email, createtime FROM t_admin WHERE loginacct = ?";

        // 执行 sql语句,获取查询结果
        Map<String, Object> resultMap = jdbcTemplate.queryForMap(sql, s);

        // 获取用户名、密码等数据
        String loginacct = resultMap.get("loginacct").toString();
        String userpswd = resultMap.get("userpswd").toString();

        // 创建权限列表,如果是角色的话就需要手动拼接 ROLE_前缀
        List<GrantedAuthority> list = AuthorityUtils.createAuthorityList("ADMIN", "USER", "ROLE_学徒");

        // 传入用户名和密码,创建 User对象并返回
        org.springframework.security.core.userdetails.User user
                = new org.springframework.security.core.userdetails.User(loginacct, userpswd, list);
        return user;
    }
}

WebSecurityConfig中使用自定义的 UserDetailsService完成登陆

@Configuration
@EnableWebSecurity // 启用 Web安全处理功能
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        auth
                .userDetailsService(userDetailsService)
        ;
    }
}

在存储 GrantedAuthority的列表中,区分 RoleAuthority的唯一准则是 RoleRole_的前缀,所以在使用自定义的 UserDetailsService登录时,在给用户添加权限时,处理 Role时要手动添加前缀。而使用内存版的时候,不能添加前缀。

实验 10:应用自定义密码加密规则

首先得保证数据库中存储的用户密码就是加密了的

SpringSecurity默认使用 BCryptPasswordEncoder实现密码的加解密。

BCryptPasswordEncoder 在加密时通过加入随机 盐值让每一次的加密结果都不同。能够避免密码的明文被猜到。

而在对明文和密文进行比较时,BCryptPasswordEncoder 会在密文的固定位置取出盐值,重新进行加密。

使用方法也很简单,只需要在 WebAppSecurityConfig中添加配置即可

@Configuration
@EnableWebSecurity // 启用 Web安全处理功能
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        auth
            .userDetailsService(userDetailsService) // 使用从数据库读取出来的用户相关信息
            .passwordEncoder(passwordEncoder) // 设置加密方式
            ;
    }
    }

实验11:在页面上显示用户昵称

在 JSP中导入标签库

<%@ taglib uri="http://www.springframework.org/security/tags" prefix="security" %>

通过标签获取已登录的用户信息

<security:authentication property="principal.originalAdmin.userName"/>

实验12:权限控制

通过配置类设置某一路径的访问权限

protected void configure(HttpSecurity http) throws Exception {
    http
        .antMatchers("/admin/get/page.html")// 针对分页显示 Admin 数据设定访问控制
        .hasRole("经理") // 要求具备经理角色
}

通过注解设置权限

@PreAuthorize("hasRole(' 部长')")
@RequestMapping("/role/get/page/info.json")
public ResultEntity<PageInfo<Role>> getPageInfo(
    @RequestParam(value="pageNum", defaultValue="1") Integer pageNum,
    @RequestParam(value="pageSize", defaultValue="5") Integer pageSize,
    @RequestParam(value="keyword", defaultValue="") String keyword
) {
    PageInfo<Role> pageInfo = roleService.getPageInfo(pageNum, pageSize, keyword);
    return ResultEntity.successWithData(pageInfo);
}

同时需要在 WebAppSecurityConfig类中添加注解

// 启用全局方法权限控制功能,并且设置 prePostEnabled = true。保证@PreAuthority、
// @PostAuthority、@PreFilter、@PostFilter 生效
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
    
}

hasAuthority也是一样的,在注解中可以这么写:

@PreAuthorize("hasRole(' 部长') OR hasAuthority('xxx')")

类似的还有 @PostAuthorize()注解,先执行方法然后根据方法返回值判断是否具备权限

例如:查询一个 Admin 对象,在@PostAuthorize 注解中和当前登录的 Admin 对象进行比较,如果不一致,则判断为不能访问。

实现“只能查自己”效果。@PostAuthorize("returnObject.data.loginAcct == principal.username")

还有 @PreFilter注解,在方法执行前对传入的参数进行过滤。只能对集合类型的数据进行过滤。

@PostFilter注解,在方法执行后对方法返回值进行过滤。只能对集合类型的数据进行过滤。

实验13:目标 9:页面元素的权限控制

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

在 JSP页面中,在需要被控制的元素外使用如下标签包裹

<security:authorize access="hasRole('经理')">
    <!-- 开始和结束标签之间是要进行权限控制的部分。检测当前用户是否有权限,有权限
就显示这里的内容,没有权限就不显示。 -->
    ……
</security:authorize>
posted @ 2021-08-31 17:01  小么VinVin  阅读(283)  评论(0编辑  收藏  举报