一、SpringSecurity框架用法简介

SpringSecurity
用户登录系统时我们需要协助 SpringSecurity 把用户对应的角色、权限组装好,同时把各个

资源所要求的权限信息设定好,剩下的“登录验证”、“权限验证”等等工作都交给 SpringSecurity。

二、权限管理过程中的相关概念

1、主体

英文单词:principal

使用系统的用户或设备或从其他系统远程登录的用户等等。简单说就是谁使用系统谁就是主体。

2、认证

英文单词:authentication

权限管理系统确认一个主体的身份,允许主体进入系统。简单说就是“主体”证明自己是谁。

笼统的认为就是以前所做的登录操作。

3、授权

英文单词:authorization

将操作系统的“权力”“授予”“主体”,这样主体就具备了操作系统中特定功能的能力。

所以简单来说,授权就是给用户分配权限。
登录并访问资源及权限验证

三、权限管理的主流框架

1、SpringSecurity

Spring 技术栈的组成部分。

通过提供完整可扩展的认证和授权支持保护你的应用程序。

SpringSecurity官方文档链接

SpringSecurity 特点:

  • 和 Spring 无缝整合。
  • 全面的权限控制。
  • 专门为 Web 开发而设计。

    • 旧版本不能脱离 Web 环境使用。
    • 新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独 引入核心模块就可以脱离 Web 环境。
  • 重量级。

2、Shiro

Apache 旗下的轻量级权限控制框架。
Shiro Logo

特点:

  • 轻量级。Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求的互联网应用有更好表现。
  • 通用性。

    • 好处:不局限于 Web 环境,可以脱离 Web 环境使用。
    • 缺陷:在 Web 环境下一些特定的需求需要手动编写代码定制。

Shiro官网
Shiro视频学习网址

四、在web项目的基础上搭建SpringSecurity环境

① 在pom.xml中导入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>

② 在web.xml中加入 SpringSecurity 控制权限的 Filter

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

③ 加入配置类 WebAppSecurityConfig.class

/**
 * @author: Herz
 * @date: 2021/7/19 12:43
 */
// 表示当前类是一个配置类
@Configuration

// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {

}

最后运行效果(这是默认行为的SpringSecurity)
在这里插入图片描述

  • 所有请求都被 SpringSecurity 拦截,要求登录才可以访问。
  • 静态资源也都被拦截,要求登录。
  • 登录失败有错误提示。
    在这里插入图片描述

五、SpringSecurity一些基本的设置

1、实现登录并访问具体资源

1.1、基于内存登录

① 需要在SpringSecurity的配置类WebAppSecurityConfig.class中重写父类方法 configure(AuthenticationManagerBuilder builder)

// 表示当前类是一个配置类
@Configuration

// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {


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

        builder
                .inMemoryAuthentication()              // 在内存中完成账号、密码的检查
                .withUser("tom")                        // 指定账号
                .password("123456")                    // 指定密码
                .roles("ADMIN")                        // 指定角色
                .and()
                .withUser("jerry")                      // 指定账号
                .password("123123")                    // 指定密码
                .authorities("UPDATE")                 // 指定权限
                ;
    }

    @Override
    protected void configure(HttpSecurity security) throws Exception {

        security
                .authorizeRequests()                       // 对请求进行授权
                .antMatchers("/index.jsp")                 // 针对 /index.jsp 路径授权
                .permitAll()                               // 可以无条件访问
                .antMatchers("/layui/**")                  // 针对 /layui/** 路径授权
                .permitAll()                               // 可以无条件访问
                .and()
                .authorizeRequests()                       // 对请求进行授权
                .anyRequest()                              // 任意请求
                .authenticated()                           //  需要登录后才可以访问
                .and()
                .formLogin()                               // 使用表单形式登录

                // 关于 loginPage() 方法的特殊说明
                // 指定登录页的同时也会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
                // /index.jsp   GET     -    去登录页
                // /index.jsp   POST    -    提交登录的表单
                // /index.jsp?error GET     -   登录失败
                // /index.jsp?logout    GET     -   退出登录
                .loginPage("/index.jsp")                                // 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)


                // loginProcessingUrl() 方法制定了提交表单地址,就会覆盖 loginPage() 方法中设置的默认值
                .loginProcessingUrl("/do/login.html")                   // 指定提交表单的地址
                .usernameParameter("loginAcct")                         // 指定登录账号的请求参数名
                .passwordParameter("userPwd")                           // 指定登录密码的请求参数名
                .defaultSuccessUrl("/main.html")                        // 指定登录成功后前往的地址
                ;                  
    }
}

② 在jsp页面中需要加入

    <p>${SPRING_SECURITY_LAST_EXCEPTION.message}</p>
    <form action="${pageContext.request.contextPath }/do/login.html" method="post">
        <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
    
    ……………………中间省略代码

    </form>

补:

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
这个在该项目中由于方便,已经禁用掉了,具体代码请关注下面代码中。

这里的 _csrf 是防止跨站请求伪造:Cross-site request forgery
即发送登录请求时没有携带_csrf 值,会报错:
在这里插入图片描述

最后的实现效果:登录成功后具体资源都可以访问了。

1.2、基于数据库的登录

① 编写一个类 SecurityAdmin

用来封装 Admin对象 和 角色、权限 信息。

/**
 *
 * 用来 封装 Admin对象 和 角色、权限 信息 的类
 *
 * 考虑到 User 对象中 仅仅包含了 账号 和 密码 ,
 * 为了能够获取到原始的 Admin 对象,专门创建这个类对 User 类进行扩展
 *
 *
 * @author: Herz
 * @date: 2021/7/21 14:49
 */

public class SecurityAdmin extends User {

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


    public SecurityAdmin(

            // 传入的原始Admin对象
            Admin originalAdmin,

            // 创建角色、权限信息的集合
            List<GrantedAuthority> authorities) {

        // 调用父类构造器
        super(originalAdmin.getUserName(),originalAdmin.getUserPwd(),authorities);

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

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


    // 对外提供的获取原始Admin对象的get()方法
    public Admin getOriginalAdmin() {
        return originalAdmin;
    }
}

② 自定义数据库查询方式

/**
 * 
 * 自定义SpringSecurity数据库查询方式
 * UserDetailsService 实现类
 *
 * @author: Herz
 * @date: 2021/7/21 15:09
 */

@Component
public class CrowdUserDetailsService implements UserDetailsService {

    @Autowired
    AdminService adminService;

    @Autowired
    RoleService roleService;

    @Autowired
    AuthService authService;


    @Override
    public UserDetails loadUserByUsername(

            // 表单提交的用户名
            String userName) throws UsernameNotFoundException {

        // 1、根据 表单 提交的用户名 查询Admin对象
        Admin admin = adminService.getAdminByLoginAcct(userName);

        // 2、获取该Admin对象的用户id
        Integer adminId = admin.getId();

        // 3、根据 adminId 获取 角色信息
        List<Role> assignedRoles = roleService.getAssignedRole(adminId);


        // 4、根据 adminId 获取权限信息
        List<String> authNames = authService.getAssignedAuthNameByAdminId(adminId);


        // 5、创建集合对象来存储 GrantedAuthority
        List<GrantedAuthority> authorities = new ArrayList<>();

        // 6、遍历assignedRoles存入角色信息
        for (Role role : assignedRoles) {

            // 不要忘记添加前缀
            String roleName = "ROLE_" + role.getName();

            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(roleName);

            authorities.add(simpleGrantedAuthority);
        }

        // 7、遍历 authNames 集合 存入权限信息
        for (String authName : authNames) {

            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authName);

            authorities.add(simpleGrantedAuthority);
        }


        // 6、封装到 SecurityAdmin 对象中
        return new SecurityAdmin(admin, authorities);
    }
}

③ 在 SpringSecurity 配置类WebAppSecurityConfig 的 configure(AuthenticationManagerBuilder builder) 方法中调用

// 表示当前类是一个配置类
@Configuration

// 启用 web 环境下的权限控制功能
@EnableWebSecurity

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

    @Autowired
    CrowdUserDetailsService userDetailsService;

    @Bean
    public BCryptPasswordEncoder getBCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

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

        // 使用基于数据库登录的账号、密码检测
        // BCryptPasswordEncoder 盐值加密
      builder.userDetailsService(userDetailsService).passwordEncoder(getBCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity security) throws Exception {

        security
                .authorizeRequests()                    // 对请求授权
                .antMatchers("/admin-login-to-page.html")       // 针对登录页进行设置
                .permitAll()                                                // 可以无条件访问
                .antMatchers("/bootstrap/**")              // 针对静态资源进行设置,可以无条件访问
                .permitAll()
                .antMatchers("/crowd/**")
                .permitAll()
                .antMatchers("/css/**")
                .permitAll()
                .antMatchers("/fonts/**")
                .permitAll()
                .antMatchers("/img/**")
                .permitAll()
                .antMatchers("/jquery/**")
                .permitAll()
                .antMatchers("/layer/**")
                .permitAll()
                .antMatchers("/script/**")
                .permitAll()
                .antMatchers("/ztree/**")
                .permitAll()

                .antMatchers("/admin-to-user-page.html") // 针对 Admin分页数据 设定访问控制
                .hasRole("经理")                                      // 具备“经理”角色

                .anyRequest()                                           // 其他任意请求
                .authenticated()                                        // 认证后才能访问


                .and()
                .csrf()                                              // 防跨站请求伪造功能
                .disable()                                          // 禁用
                .formLogin()                                         // 使用表单形式登录
                .loginPage("/admin-login-to-page.html")                        // 指定默认的登录页
                .permitAll()
                .loginProcessingUrl("/security-do-login-page.html")  // 指定处理登录请求的地址
                .permitAll()
                .usernameParameter("login_acct")                     // 指定登录账号的 请求参数名
                .passwordParameter("login_password")                 // 指定登录密码的 请求参数名
                .defaultSuccessUrl("/admin-main-to-page.html")       // 登录成功后前往的地址

                .and()
                .logout()                                           // 开启退出功能
                .logoutUrl("/security-do-logout-page.html")         // 处理退出请求的地址
                .logoutSuccessUrl("/admin-login-to-page.html")      // 成功退出后页面跳转的地址
        ;


    }
}

2、退出登录

2.1、禁用CSRF情况下退出

① 重写父类方法 指定处理退出请求的URL地址 和 成功退出后前往的地址 并禁用CSRF

// 表示当前类是一个配置类
@Configuration

// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {


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

        builder
                .inMemoryAuthentication()              // 在内存中完成账号、密码的检查
                .withUser("tom")                        // 指定账号
                .password("123456")                    // 指定密码
                .roles("ADMIN")                        // 指定角色
                .and()
                .withUser("jerry")                      // 指定账号
                .password("123123")                    // 指定密码
                .authorities("UPDATE")                 // 指定权限
                ;
    }

    @Override
    protected void configure(HttpSecurity security) throws Exception {

        security
                .authorizeRequests()                       // 对请求进行授权
                .antMatchers("/index.jsp")                 // 针对 /index.jsp 路径授权
                .permitAll()                               // 可以无条件访问
                .antMatchers("/layui/**")                  // 针对 /layui/** 路径授权
                .permitAll()                               // 可以无条件访问
                .and()
                .authorizeRequests()                       // 对请求进行授权
                .anyRequest()                              // 任意请求
                .authenticated()                           //  需要登录后才可以访问
                .and()
                .formLogin()                               // 使用表单形式登录

                // 关于 loginPage() 方法的特殊说明
                // 指定登录页的同时也会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
                // /index.jsp   GET     -    去登录页
                // /index.jsp   POST    -    提交登录的表单
                // /index.jsp?error GET     -   登录失败
                // /index.jsp?logout    GET     -   退出登录
                .loginPage("/index.jsp")                                // 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)


                // loginProcessingUrl() 方法制定了提交表单地址,就会覆盖 loginPage() 方法中设置的默认值
                .loginProcessingUrl("/do/login.html")                   // 指定提交表单的地址
                .usernameParameter("loginAcct")                         // 指定登录账号的请求参数名
                .passwordParameter("userPwd")                           // 指定登录密码的请求参数名
                .defaultSuccessUrl("/main.html")                        // 指定登录成功后前往的地址
                ;


                .and()
                .csrf()
                .disable()                                              // 禁用CSRF
                .logout()                                               // 开启退出功能
                .logoutUrl("/do/logout.html")                           // 指定处理退出请求的URL地址
                .logoutSuccessUrl("/index.jsp")                         // 成功退出后前往的地址
                ;                                    
    }
}

② 表单退出按钮

<a href="${pageContext.request.contextPath }/do/logout.html">退出</a>

2.2、开启CSRF的情况下退出

① 重写父类方法 指定处理退出请求的URL地址 和 成功退出后前往的地址

// 表示当前类是一个配置类
@Configuration

// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {


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

        builder
                .inMemoryAuthentication()              // 在内存中完成账号、密码的检查
                .withUser("tom")                        // 指定账号
                .password("123456")                    // 指定密码
                .roles("ADMIN")                        // 指定角色
                .and()
                .withUser("jerry")                      // 指定账号
                .password("123123")                    // 指定密码
                .authorities("UPDATE")                 // 指定权限
                ;
    }

    @Override
    protected void configure(HttpSecurity security) throws Exception {

        security
                .authorizeRequests()                       // 对请求进行授权
                .antMatchers("/index.jsp")                 // 针对 /index.jsp 路径授权
                .permitAll()                               // 可以无条件访问
                .antMatchers("/layui/**")                  // 针对 /layui/** 路径授权
                .permitAll()                               // 可以无条件访问
                .and()
                .authorizeRequests()                       // 对请求进行授权
                .anyRequest()                              // 任意请求
                .authenticated()                           //  需要登录后才可以访问
                .and()
                .formLogin()                               // 使用表单形式登录

                // 关于 loginPage() 方法的特殊说明
                // 指定登录页的同时也会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
                // /index.jsp   GET     -    去登录页
                // /index.jsp   POST    -    提交登录的表单
                // /index.jsp?error GET     -   登录失败
                // /index.jsp?logout    GET     -   退出登录
                .loginPage("/index.jsp")                                // 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)


                // loginProcessingUrl() 方法制定了提交表单地址,就会覆盖 loginPage() 方法中设置的默认值
                .loginProcessingUrl("/do/login.html")                   // 指定提交表单的地址
                .usernameParameter("loginAcct")                         // 指定登录账号的请求参数名
                .passwordParameter("userPwd")                           // 指定登录密码的请求参数名
                .defaultSuccessUrl("/main.html")                        // 指定登录成功后前往的地址
                ;


                .and()
                .logout()                                               // 开启退出功能
                .logoutUrl("/do/logout.html")                           // 指定处理退出请求的URL地址
                .logoutSuccessUrl("/index.jsp")                         // 成功退出后前往的地址
                ;                                    
    }
}

② 表单退出按钮

            <form id="logoutForm" action="${pageContext.request.contextPath }/do/logout.html" method="post">
                <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
            </form>
            <a id="logoutAnchor" href="">退出</a>
            <script type="text/javascript">
                window.onload = function() {

                    // 给超链接的DOM对象绑定单击响应函数
                    document.getElementById("logoutAnchor").onclick = function() {

                        // 提交包含csrf参数的表单
                        document.getElementById("logoutForm").submit();

                        // 取消超链接的默认行为
                        return false;

                    };

                };
            </script>

3、基于角色或权限访问控制

① 基于配置类

// 表示当前类是一个配置类
@Configuration

// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {


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

        builder
                .inMemoryAuthentication()              // 在内存中完成账号、密码的检查
                .withUser("tom")                        // 指定账号
                .password("123456")                    // 指定密码
                .roles("ADMIN","学徒")                        // 指定角色
                .and()
                .withUser("jerry")                      // 指定账号
                .password("123123")                    // 指定密码
                .authorities("UPDATE","内门弟子")                 // 指定权限
                ;
    }

    @Override
    protected void configure(HttpSecurity security) throws Exception {

        security
                .authorizeRequests()                       // 对请求进行授权
                .antMatchers("/index.jsp")                 // 针对 /index.jsp 路径授权
                .permitAll()                               // 可以无条件访问
                .antMatchers("/layui/**")                  // 针对 /layui/** 路径授权
                .permitAll()                               // 可以无条件访问

                .antMatchers("/level1/**")                 // 针对 /level1/** 路径设置访问要求
                .hasRole("学徒")                            // 要求具备“学徒”角色才能访问
                .antMatchers("/level2/**")                 // 针对 /level2/** 路径设置访问要求
                .hasAuthority("内门弟子")                   // 要求具备“内门弟子”权限才能访问



                .and()
                .authorizeRequests()                       // 对请求进行授权
                .anyRequest()                              // 任意请求
                .authenticated()                           //  需要登录后才可以访问
                .and()
                .formLogin()                               // 使用表单形式登录

                // 关于 loginPage() 方法的特殊说明
                // 指定登录页的同时也会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
                // /index.jsp   GET     -    去登录页
                // /index.jsp   POST    -    提交登录的表单
                // /index.jsp?error GET     -   登录失败
                // /index.jsp?logout    GET     -   退出登录
                .loginPage("/index.jsp")                                // 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)


                // loginProcessingUrl() 方法制定了提交表单地址,就会覆盖 loginPage() 方法中设置的默认值
                .loginProcessingUrl("/do/login.html")                   // 指定提交表单的地址
                .usernameParameter("loginAcct")                         // 指定登录账号的请求参数名
                .passwordParameter("userPwd")                           // 指定登录密码的请求参数名
                .defaultSuccessUrl("/main.html")                        // 指定登录成功后前往的地址
                ;


                .and()
                .logout()                                               // 开启退出功能
                .logoutUrl("/do/logout.html")                           // 指定处理退出请求的URL地址
                .logoutSuccessUrl("/index.jsp")                         // 成功退出后前往的地址
                ;                                    
    }
}

!!!注意:通过观察源码不难发现,通过角色(role)来进行访问控制时,在传参的时候会自动的拼串加上"ROLE_"前缀,而通过权限(auth)控制则没有。

因此,将来从数据库查询得到的用户信息、角色信息、权 限信息需要我们自己手动组装。手动组装时需要我们自己给角色字符串前面加“ROLE_” 前缀。

② 基于注解

1、要在SpringSecurity的配置类上加上注解 @EnableGlobalMethodSecurity

// 表示当前类是一个配置类
@Configuration

// 启用 web 环境下的权限控制功能
@EnableWebSecurity

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


}

2、在要设置权限访问的请求上添加注解 @PreAuthorize()

    @PreAuthorize("hasAuthority('user:save')")        // 具备 user:save 权限才能访问
    @RequestMapping("/admin-save.html")
    public String saveAdmin(Admin admin) {

        adminService.saveAdmin(admin);

        return "redirect:/admin-to-user-page.html?pageNum=" + Integer.MAX_VALUE;
    }

    @PreAuthorize("hasRole('部长')")    // 具备部长角色才能访问 Role分页数据页面
    @ResponseBody
    @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
    ) {

        // 调用service 里的方法获取数据信息
        PageInfo<Role> pageInfo = roleService.getPageInfo(pageNum, pageSize, keyword);

        // 封装到 ResultEntity 对象里
        return ResultEntity.successWithData(pageInfo);
    }

4、自定义403异常页面

4.1、重写父类方法

指定访问被拒绝时前往的页面

// 表示当前类是一个配置类
@Configuration

// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {


    

    @Override
    protected void configure(HttpSecurity security) throws Exception {

        security
                .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);
                    }
                })
                ;                  
    }
}

4.2、编写自定义403异常页面

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
    pageContext.setAttribute("PATH", request.getContextPath());
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport"
    content="width=device-width, initial-scale=1, maximum-scale=1">
<title>武林秘籍管理系统</title>
<link rel="stylesheet" href="${PATH }/layui/css/layui.css">
</head> 
<body class="layui-layout-body">
    <div class="layui-layout layui-layout-admin">
        <!-- 顶部导航 -->
        <%@include file="/WEB-INF/include/navbar.jsp" %>
        
        <!-- 侧边栏 -->
        <%@include file="/WEB-INF/include/sidebar.jsp" %>
        
        
        <div class="layui-body">
            <!-- 内容主体区域 -->
            <div style="padding: 15px;">
                <h1>非常抱歉!您没有访问这个功能的权限!</h1>
                <h2>${message }</h2>
            </div>
        </div>
        <div class="layui-footer"></div>
    </div>
    <script src="${PATH }/layui/layui.js"></script>
    <script>
        //JavaScript代码区域
        layui.use('element', function() {
            var element = layui.element;

        });
    </script>
</body>
</html>

5、记住我

5.1、内存版

HttpSecurity 对象调用 rememberMe()方法。 登录表单携带名为 remember-me 的请求参数。具体做法是将登录表单中的 checkbox 的 name 设置为 remember-me.

如 果 不 能 使 用 “ remember-me ” 作 为 请 求 参 数 名 称 , 可 以 使 用HttpSecurity 对象调用 rememberMeParameter()方法定制。

① 重写父类方法,开启记住我功能

// 表示当前类是一个配置类
@Configuration

// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity security) throws Exception {
        security.rememberMe();            // 开启“记住我”功能
                             
    }
}

② 页面勾选框“记住我”

 <input type="checkbox" name="remember-me" lay-skin="primary"
                           title="记住我"> <a href="forget.html"
                                           class="layadmin-user-jump-change layadmin-link"
                                           style="margin-top: 7px;">忘记密码?</a>

5.2、数据库版

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

① 在此之前,需要导入数据库相关依赖以及在spring-mvc.xml中配置数据源

<!-- 配置数据源 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="username" value="root"></property>
        <property name="password" value="tianfei"></property>
        <property name="url" value="jdbc:mysql://localhost:3306/security?useSSL=false"></property>
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
    </bean>

    <!-- jdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

② 创建数据库
CREATE DATABASE security CHARACTER SET utf8;

③ 在 WebAppSecurityConfig 类中注入数据源

@Autowired private 
DataSource dataSource;

!!!注意在JdbcTokenRepositoryImpl 类中由于创建表的initDao()函数是protected,不能再外部调用,所以要进行一定的操作(有三种方法):

  • 可以写一个类继承JdbcTokenRepositoryImpl 类
  • 修改JdbcTokenRepositoryImpl 类的源码
  • 自己创建同结构的表

我这里选择的是修改源码(不建议)

④ 修改的源码部分(没写出来的部分不用修改)如下

public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements PersistentTokenRepository {
    public static final String CREATE_TABLE_SQL = "create table if not exists persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)";


    public void initDao() {
            if (this.createTableOnStartup) {
                this.getJdbcTemplate().execute("create table if not exists persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)");
        }    

   }
}

⑤ 具体的SpringSecurity配置类WebAppSecurityConfig 代码如下

/**
 *
 * SpringSecurity的配置类
 *
 * @author: Herz
 * @date: 2021/7/19 12:43
 */
// 表示当前类是一个配置类
@Configuration

// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private MyUserDetailsService userDetailsService;

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

        // 装配 userDetailsService
        builder
                .userDetailsService(userDetailsService);
    }

    @Override
    protected void configure(HttpSecurity security) throws Exception {

        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);

        // 开启自动创建表功能
        tokenRepository.setCreateTableOnStartup(true);

        // 自动创建表方法
        tokenRepository.initDao();

        security
                .authorizeRequests()                                    // 对请求进行授权
                .antMatchers("/index.jsp")                 // 针对 /index.jsp 路径授权
                .permitAll()                                            // 可以无条件访问
                .antMatchers("/layui/**")                  // 针对 /layui/** 路径授权
                .permitAll()                                            // 可以无条件访问


                .antMatchers("/level1/**")                 // 针对 /level1/** 路径设置访问要求
                .hasRole("学徒")                                        // 要求具备“学徒”角色才能访问
                .antMatchers("/level2/**")                 // 针对 /level2/** 路径设置访问要求
                .hasAuthority("内门弟子")                               // 要求具备“内门弟子”权限才能访问


                .and()
                .authorizeRequests()                                    // 对请求进行授权
                .anyRequest()                                           // 任意请求
                .authenticated()                                        //  需要登录后才可以访问
                .and()
                .formLogin()                                            // 使用表单形式登录

                // 关于 loginPage() 方法的特殊说明
                // 指定登录页的同时也会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
                // /index.jsp   GET     -    去登录页
                // /index.jsp   POST    -    提交登录的表单
                // /index.jsp?error GET     -   登录失败
                // /index.jsp?logout    GET     -   退出登录
                .loginPage("/index.jsp")                                // 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)


                // loginProcessingUrl() 方法制定了提交表单地址,就会覆盖 loginPage() 方法中设置的默认值
                .loginProcessingUrl("/do/login.html")                   // 指定提交表单的地址
                .usernameParameter("loginAcct")                         // 指定登录账号的请求参数名
                .passwordParameter("userPwd")                           // 指定登录密码的请求参数名
                .defaultSuccessUrl("/main.html")                        // 指定登录成功后前往的地址


//                .and()
//                .csrf()
//                .disable()                                              // 禁用CSRF
                .and()
                .logout()                                               // 开启退出功能

                .logoutSuccessUrl("/index.jsp")                         // 成功退出后前往的地址

                .and()
                .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);
                    }
                })
                .and()
                .rememberMe()                                       // 开启“记住我”功能
                .tokenRepository(tokenRepository)
                ;                  
    }
}

6、登录密码加密

6.1、普通加密

① 编写一个类继承 PasswordEncoder 类 并实现抽象方法

/**
 *
 * 用于登录密码检测
 *
 * @author: Herz
 * @date: 2021/7/20 9:47
 */

@Component
public class MyPasswordEncoder implements PasswordEncoder {

    /**
     * 给登录密码加密操作
     * @param rowPassword 登录密码
     * @return
     */
    @Override
    public String encode(CharSequence rowPassword) {

        try {
            // 1、创建 MessageDigest 对象
            String algorithm = "MD5";
            MessageDigest messageDigest = MessageDigest.getInstance(algorithm);

            // 2、获取 rowPassword 的字节数组
            byte[] input = ((String) rowPassword).getBytes();

            // 3、加密
            byte[] output = messageDigest.digest(input);

            // 4、转换为16进制数对应的字符  大写
            String encoded = new BigInteger(1, output).toString(16).toUpperCase();

            return encoded;

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 比较登录密码和数据库密码是否一致
     * @param rowPassword 登录明文密码
     * @param encodePassword 数据库加密密码
     * @return
     */
    @Override
    public boolean matches(CharSequence rowPassword, String encodePassword) {

        // 1、明文密码加密
        String formPassword = encode(rowPassword);

        // 2、声明数据库密码
        String dataBasePassword = encodePassword;

        // 3、比较密码是否一致
        return Objects.equals(formPassword,dataBasePassword);
    }
}

②在 SpringSecurity 配置类中 通过 AuthenticationManagerBuilder 对象 调用 userDetailsService(userDetailsService).passwordEncoder(passwordEncoder) 方法

/**
 *
 * SpringSecurity的配置类
 *
 * @author: Herz
 * @date: 2021/7/19 12:43
 */
// 表示当前类是一个配置类
@Configuration

// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private MyUserDetailsService userDetailsService;

    @Autowired
    private MyPasswordEncoder passwordEncoder;

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
    
        // 装配 userDetailsService
        builder
                .userDetailsService(userDetailsService)      // 用于用户登录检测
                .passwordEncoder(passwordEncoder);             // 普通密码加密

    }

6.2、带盐值加密

直接在 SpringSecurity 配置类中 通过 AuthenticationManagerBuilder 对象 调用 userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder()) 方法

/**
 *
 * SpringSecurity的配置类
 *
 * @author: Herz
 * @date: 2021/7/19 12:43
 */
// 表示当前类是一个配置类
@Configuration

// 表示启用 SpringSecurity 的 web 功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private MyUserDetailsService userDetailsService;

    @Autowired
    private MyPasswordEncoder passwordEncoder;

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
    
        // 装配 userDetailsService
        builder
                .userDetailsService(userDetailsService)      // 用于用户登录检测
                .passwordEncoder(new BCryptPasswordEncoder());    // 带盐值密码加密

    }

7、页面元素的访问控制

例如:
页面元素的访问控制
① 在页面引入标签库

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

② 给页面元素添加角色或权限

                <%-- 这里设置的是角色
                    
                    设置权限:<security:authorize access="hasAuthority('role:add')">
                 --%>
                <security:authorize access="hasRole('经理')">
                    <div class="col-xs-6 col-sm-3 placeholder">
                        <img data-src="holder.js/200x200/auto/sky" class="img-responsive"
                             alt="Generic placeholder thumbnail">
                        <h4>Label</h4>
                        <span class="text-muted">Something else</span>
                    </div>
                </security:authorize>

效果:使用不具备该角色的用户登录时查看不到该页面元素
不具备相应角色

最后修改:2021 年 08 月 01 日 05 : 50 PM
如果觉得我的文章对你有用,请随意赞赏