• SpringSecurity整合SSM和SpringBoot完成方法级权限控制


    初识权限管理

    权限管理,一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源。权限管理几乎出现在任何系统里面,前提是需要有用户和密码认证的系统。

    在权限管理的概念中,有两个非常重要的名词:
    认证:通过用户名和密码成功登陆系统后,让系统得到当前用户的角色身份。
    授权:系统根据当前用户的角色,给其授予对应可以操作的权限资源。

    完成权限管理需要三个对象:

    • 用户:主要包含用户名,密码和当前用户的角色信息,可实现认证操作。
    • 角色:主要包含角色名称,角色描述和当前角色拥有的权限信息,可实现授权操作。
    • 权限:权限也可以称为菜单,主要包含当前权限名称,url地址等信息,可实现动态展示菜单。

    这也是经典的 RBAC 模式:

    RBAC 是基于角色的访问控制Role-Based Access Control )在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。

    初识Spring Security

    Spring Securityspring 采用AOP思想,基于 servlet 过滤器实现的安全框架。它提供了完善的认证机制方法级的授权功能。是一款非常优秀的权限管理框架。

    入门案例

    添加 SpringSecurity 坐标

    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-webmvcartifactId>
        <version>5.1.5.RELEASEversion>
    dependency>
    <dependency>
        <groupId>org.springframework.securitygroupId>
        <artifactId>spring-security-webartifactId>
        <version>5.1.5.RELEASEversion>
    dependency>
    <dependency>
        <groupId>org.springframework.securitygroupId>
        <artifactId>spring-security-configartifactId>
        <version>5.1.5.RELEASEversion>
    dependency>
    
    <dependency>
        <groupId>ch.qos.logbackgroupId>
        <artifactId>logback-classicartifactId>
        <version>1.2.3version>
    dependency>
    <dependency>
        <groupId>javax.servletgroupId>
        <artifactId>javax.servlet-apiartifactId>
        <version>4.0.1version>
        <scope>providedscope>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    核心文件配置 - mvc 资源启用

    <context:component-scan base-package="org.neuedu.security.demo.controller" />
    
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/pages/"/>
        <property name="suffix" value=".jsp" />
    bean>
    
    <mvc:annotation-driven />
    <mvc:default-servlet-handler />
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    核心文件配置 - 认证和资源拦截

    
    
    
    <security:http >
        
        <security:intercept-url pattern="/**" access="hasRole('ROLE_ADMIN')"/>
        <security:form-login />
        
    security:http>
    
    
    <security:authentication-manager>
        <security:authentication-provider>
            
            <security:user-service>
                
               <security:user name="user" password="{noop}user" authorities="ROLE_USER" />
               <security:user name="admin" password="{noop}admin" authorities="ROLE_ADMIN" />
            security:user-service>
        security:authentication-provider>
    security:authentication-manager>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    auto-config="true" 自动配置,加上它,则可以省略 认证方式,即 可以不用
    use-expressions="true" 启用 SPEL 表达式,页面可以获取响应的认证对象
    页面表单的形式认证
    页面弹出框的形式认证

    配置 SpringSecurity 过滤器

    
    <filter>
        <filter-name>springSecurityFilterChainfilter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxyfilter-class>
    filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChainfilter-name>
        <url-pattern>/*url-pattern>
    filter-mapping>
    
    
    <servlet>
        <servlet-name>springMVCservlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
        <init-param>
            <param-name>contextConfigLocationparam-name>
            <param-value>classpath:application-*.xmlparam-value>
        init-param>
        
        <load-on-startup>1load-on-startup>
    servlet>
    
    <servlet-mapping>
        <servlet-name>springMVCservlet-name>
        <url-pattern>/url-pattern>
    servlet-mapping>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    默认首页 index.jsp

    <h1>SpringSecurity 权限管理 <a href="/logout">退出a>h1>
    <p><a href="dept/add">添加部门a>p>
    <p><a href="dept/del">删除部门a>p>
    <p><a href="dept/edit">修改部门a>p>
    <p><a href="dept/list">部门列表a>p>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    运行项目首页查看

    在这里插入图片描述

    运行可以看到,系统并没有进入我们期待的 index.jsp 页面,而是进入了一个登录页面,这个页面是由 SpringSecurity 框架为我们提供的,我们自己配置了拦截所有资源,也就是说,所有资源请求都需要认证通过才能继续访问。而对应的用户名和密码就是我们自己配置的 adminuser.

    在这个登录页面上输入用户名 user,密码 user ,点击 Sign in,这样就可以进入 index.jsp 页面了。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bROGRUz2-1668589974349)(.\images\1575021648945.png)]

    到此,我们就完成了一个入门案例,但是我们在实际开发过程中,肯定不可能使用 SpringSecurity 提供的这个默认登录页面,不然不仅项目色调不一致,语言类型也不一致。

    SpringSecurity使用自定义认证页面

    查看 SringSecurity 默认提供的登录界面,获取对应的默认数据: 请求方式字段名称 , 请求地址

    SpringSecurity 主配置文件中指定认证页面配置信息,注意:登录页面需要放行,否则会出现死循环

    
    
    <security:http pattern="/fail.jsp" security="none" />
    
    <security:http auto-config="true" use-expressions="true">
        
        <security:intercept-url pattern="/login.jsp" access="permitAll()" />
        <security:intercept-url pattern="/dept/add" access="hasRole('ROLE_USER')" />
        <security:intercept-url pattern="/dept/del" access="hasRole('ROLE_ADMIN')" />
        <security:intercept-url pattern="/dept/edit" access="hasRole('ROLE_ADMIN')" />
        <security:intercept-url pattern="/dept/list" access="hasAnyRole('ROLE_ADMIN','ROLE_USER')" />
       
       <security:intercept-url pattern="/**" access="isFullyAuthenticated()" />
    
        
        <security:form-login login-page="/login.jsp"
                             username-parameter="username" 
                             password-parameter="password"
                             login-processing-url="/login"
                             default-target-url="/index.jsp"
                             authentication-failure-url="/fail.jsp" />
        
        <security:logout logout-url="/logout" logout-success-url="/login.jsp" />
        
        
        <security:csrf disabled="true" />
    
         
        <security:access-denied-handler error-page="/fail.jsp" />
        
    security:http>
    
    
    
    <security:authentication-manager>
        <security:authentication-provider>
            <security:user-service>
               <security:user name="user" password="{noop}user" authorities="ROLE_USER" />
               <security:user name="admin" password="{noop}admin" authorities="ROLE_ADMIN" />
            security:user-service>
        security:authentication-provider>
    security:authentication-manager>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    SpringSecurity 中的 SpringEL表达式
    表达式	                       	  =			描述
    hasRole([role])	          		=当前用户是否拥有指定角色。
    hasAnyRole([role1,role2])  		=多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任									意一个则返回true。
    hasAuthority([auth])	   		= 等同于hasRole
    hasAnyAuthority([auth1,auth2])	=等同于hasAnyRole
    Principle	                  	=代表当前用户的principle对象
    authentication	              	=直接从SecurityContext获取的当前Authentication对象
    permitAll	   				 	=总是返回true,表示允许所有的
    denyAll							=总是返回false,表示拒绝所有的
    isAnonymous()					=当前用户是否是一个匿名用户
    isRememberMe()					=表示当前用户是否是通过Remember-Me自动登录的
    isAuthenticated()				=表示当前用户是否已经登录认证成功了。
    isFullyAuthenticated()			=如果当前用户既不是一个匿名用户,同时又不是通过Remember-Me自动登录									的,则返回true。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述

    403什么异常?这是 SpringSecurity 中的权限不足!这个异常怎么来的?还记得上面登录页面源码中的那个_csrf隐藏input吗?问题就在这了!

    SpringSecurity的csrf防护机制

    CSRF(Cross-site request forgery)跨站请求伪造,是一种难以防范的网络攻击方式。

    SpringSecurity 的 csrf 机制把请求方式分成两类来处理 - 【 CsrfFilter 】。
    第一类 :“GET”, “HEAD”, “TRACE”, "OPTIONS"四类请求可以直接通过.
    第二类 :除去上面四类,包括 POST 都要被验证携带 token 或者 关闭 csrf 防护才能通过.

    • SpringSecurity 主配置文件中添加禁用crsf防护的配置 :
    • 在认证页面携带 token 请求: 导入 SpringSecurity 标签库,在表单中录入:

    注意:一旦开启了csrf防护功能,logout处理器便只支持POST请求方式了!

    SpringSecurity 原理分析

    SpringSecurity 流程图

    在这里插入图片描述

    1. 客户端发起一个请求,进入 Security 过滤器链
    2. 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。
    3. 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
    4. 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。

    Spring Security常用过滤器介绍

    • SecurityContextPersistenceFilter : 在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。
    • CsrfFilter : 用于处理跨站请求伪造
    • UsernamePasswordAuthenticationFilter : 用于处理基于表单的登录请求,从表单中获取用户名和密码。认证操作全靠这个过滤器,默认匹配URL为 /login 且必须为 POST 请求, 默认使用的表单 name 值为 usernamepassword ,这两个值可以通过设置这个过滤器的 usernameParameterpasswordParameter 两个参数的值进行修改。
    • DefaultLoginPageGeneratingFilter : 如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。
    • LogoutFilter : 匹配URL为 /logout 的请求,实现用户退出,清除认证信息。
    • DefaultLogoutPageGeneratingFilter : 由此过滤器可以生产一个默认的退出登录页面
    • AnonymousAuthenticationFilter : 当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到 SecurityContextHolder 中。
    • FilterSecurityInterceptor : 以看做过滤器链的出口。获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限。
    • RememberMeAuthenticationFilter : 当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的 remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启

    核心类简介

    • Authentication 是一个接口,用来表示用户认证信息的,在用户登录认证之前相关信息会封装为一个 Authentication 具体实现类的对象,在登录认证成功之后又会生成一个信息更全面,包含用户权限等信息的 Authentication 对象,然后把它保存在 SecurityContextHolder 所持有的 SecurityContext 中,供后续的程序进行调用,如访问权限的鉴定等。

    • SecurityContextHolder 是用来保存 SecurityContext 的。SecurityContext 中含有当前正在访问系统的用户的详细信息。

    • AuthenticationManager 是一个用来处理认证(Authentication)请求的接口。在其中只定义了一个方法 authenticate(),该方法只接收一个代表认证请求的 Authentication 对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的 Authentication 对象进行返回。

      校验认证请求最常用的方法是根据请求的用户名加载对应的 UserDetails,然后比对 UserDetails 的密码与认证请求的密码是否一致,一致则表示认证通过。在认证成功以后会使用加载的 UserDetails 来封装要返回的 Authentication 对象,加载的 UserDetails 对象是包含用户权限等信息的。认证成功返回的 Authentication 对象将会保存在当前的 SecurityContext 中。

    • UserDetailsService 通过 Authentication.getPrincipal() 返回的其实是一个 UserDetails 实例UserDetailsSpring Security 中一个核心的接口。其中定义了一些可以获取用户名、密码、权限等与认证相关的信息的方法。Spring Security 内部使用的 UserDetails 实现类大都是内置的 User 类,我们如果要使用 UserDetails 时也可以直接使用该类。

    UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext()
                                                    .getAuthentication().getPrincipal();
    String username = userDetails.getUsername();
    
    • 1
    • 2
    • 3

    初识自定义认证

    UsernamePasswordAuthenticationFilter 拦截认证, 请求必须是 POST , 填写的用户名(username)和密码(password)会封装到 UsernamePasswordAuthenticationToken 中 , 调用 AuthenticationManager 对象实现认证. 实现类 AuthenticationProvider 完成认证业务,我们可以直接编写一个 UserDetailsService 的实现类返回一个UserDetails 对象即可。这里需要注意返回的对象中需要带有权限信息

    //自定义认证业务逻辑
    public class UserService implements UserDetailsService {
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            //User(String username, String password, Collection authorities)
            //User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection authorities)
            List<GrantedAuthority> roles = new ArrayList<>();
            roles.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
            roles.add(new SimpleGrantedAuthority("ROLE_USER"));
            //User user = new User("admin","{noop}admin",roles);
            User user = new User("admin","{noop}admin",true,true,true,true,roles);
            return user;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    设置用户状态

    在用户认证业务里,认证过程中,这四个参数必须同时为 true 认证才能通过,当然这四个字段我们也可以把它添加到数据库字段中,这样也可以完成动态设置.

    • boolean enabled 是否可用
    • boolean accountNonExpired 账户是否失效
    • boolean credentialsNonExpired 认证是否过期
    • boolean accountNonLocked 账户是否锁定

    使用数据库完成动态认证

    上面的案例我们是自己模拟出一个用户信息和角色权限,接下来我们使用数据库中动态的数据来完成认证,其实很简单我们只需要在数据库中添加对应的用户表和角色表,然后添加两个方法 根据用户名查询用户信息 , 根据用户ID查询角色集合 , 然后动态的去替换上面 UserService 中的模拟数据即可。接下来我们开始搭建后端环境。

    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
        <version>6.0.6version>
    dependency>
    <dependency>
        <groupId>com.baomidougroupId>
        <artifactId>mybatis-plusartifactId>
        <version>3.1.0version>
    dependency>
    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
        <version>1.16.16version>
    dependency>
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-jdbcartifactId>
        <version>5.1.5.RELEASEversion>
    dependency>
    <dependency>
        <groupId>ch.qos.logbackgroupId>
        <artifactId>logback-classicartifactId>
        <version>1.2.3version>
    dependency>
    <dependency>
        <groupId>junitgroupId>
        <artifactId>junitartifactId>
        <version>4.12version>
    dependency>
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-testartifactId>
        <version>5.1.5.RELEASEversion>
    dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.coregroupId>
        <artifactId>jackson-databindartifactId>
        <version>2.9.6version>
    dependency>
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>druidartifactId>
        <version>1.1.14version>
    dependency>
    
    
    <dependency>
        <groupId>com.baomidougroupId>
        <artifactId>mybatis-plus-generatorartifactId>
        <version>3.1.0version>
    dependency>
    
    <dependency>
        <groupId>org.freemarkergroupId>
        <artifactId>freemarkerartifactId>
        <version>2.3.28version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    后端数据库整合

    RBAC 数据表结构

    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;
    
    -- ----------------------------
    -- Table structure for sys_permission
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_permission`;
    CREATE TABLE `sys_permission`  (
      `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
      `permission_NAME` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '菜单名称',
      `permission_url` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '菜单地址',
      `parent_id` int(11) NOT NULL DEFAULT 0 COMMENT '父菜单id',
      PRIMARY KEY (`ID`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
    
    -- ----------------------------
    -- Records of sys_permission
    -- ----------------------------
    INSERT INTO `sys_permission` VALUES (1, '部门添加 ', '/dept/add', 0);
    INSERT INTO `sys_permission` VALUES (2, '部门删除', '/dept/del', 0);
    INSERT INTO `sys_permission` VALUES (3, '部门修改', '/dept/edit', 0);
    INSERT INTO `sys_permission` VALUES (4, '部门列表', '/dept/list', 0);
    
    -- ----------------------------
    -- Table structure for sys_role
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_role`;
    CREATE TABLE `sys_role`  (
      `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
      `ROLE_NAME` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '角色名称',
      `ROLE_DESC` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '角色描述',
      PRIMARY KEY (`ID`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
    
    -- ----------------------------
    -- Records of sys_role
    -- ----------------------------
    INSERT INTO `sys_role` VALUES (1, 'ROLE_ADMIN', '管理员角色');
    INSERT INTO `sys_role` VALUES (2, 'ROLE_USER', '普通用户角色');
    
    -- ----------------------------
    -- Table structure for sys_role_permission
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_role_permission`;
    CREATE TABLE `sys_role_permission`  (
      `RID` int(11) NOT NULL COMMENT '角色编号',
      `PID` int(11) NOT NULL COMMENT '权限编号',
      PRIMARY KEY (`RID`, `PID`) USING BTREE,
      INDEX `FK_Reference_12`(`PID`) USING BTREE,
      CONSTRAINT `FK_Reference_11` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT,
      CONSTRAINT `FK_Reference_12` FOREIGN KEY (`PID`) REFERENCES `sys_permission` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
    
    -- ----------------------------
    -- Records of sys_role_permission
    -- ----------------------------
    INSERT INTO `sys_role_permission` VALUES (1, 1);
    INSERT INTO `sys_role_permission` VALUES (1, 2);
    INSERT INTO `sys_role_permission` VALUES (2, 3);
    INSERT INTO `sys_role_permission` VALUES (2, 4);
    
    -- ----------------------------
    -- Table structure for sys_user
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_user`;
    CREATE TABLE `sys_user`  (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名称',
      `password` varchar(120) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码',
      `status` int(1) DEFAULT 1 COMMENT '1开启0关闭',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
    
    -- ----------------------------
    -- Records of sys_user
    -- ----------------------------
    INSERT INTO `sys_user` VALUES (1, 'user', '{noop}user', 1);
    INSERT INTO `sys_user` VALUES (2, 'admin', '{noop}admin', 1);
    
    -- ----------------------------
    -- Table structure for sys_user_role
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_user_role`;
    CREATE TABLE `sys_user_role`  (
      `UID` int(11) NOT NULL COMMENT '用户编号',
      `RID` int(11) NOT NULL COMMENT '角色编号',
      PRIMARY KEY (`UID`, `RID`) USING BTREE,
      INDEX `FK_Reference_10`(`RID`) USING BTREE,
      CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT,
      CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `sys_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
    
    -- ----------------------------
    -- Records of sys_user_role
    -- ----------------------------
    INSERT INTO `sys_user_role` VALUES (2, 1);
    INSERT INTO `sys_user_role` VALUES (1, 2);
    INSERT INTO `sys_user_role` VALUES (2, 2);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98

    MP 代码生成器

    //读取属性配置文件
    private ResourceBundle rb = ResourceBundle.getBundle("druid");
    
    @Test
    public void codeGenerator(){
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();
    
        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/src/main/java");
        gc.setAuthor("CDHong");
        gc.setOpen(false);
        gc.setSwagger2(false); //实体属性 Swagger2 注解
        mpg.setGlobalConfig(gc);
    
        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl(rb.getString("url"));
        // dsc.setSchemaName("public");
        dsc.setDriverName(rb.getString("driver"));
        dsc.setUsername(rb.getString("user"));
        dsc.setPassword(rb.getString("pwd"));
        mpg.setDataSource(dsc);
    
        // 包配置
        PackageConfig pc = new PackageConfig();
        //父级公用包名,就是自动生成的文件放在项目路径下的那个包中
        pc.setParent("org.neuedu.spring.security.demo");
        mpg.setPackageInfo(pc);
    
        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };
        String templatePath = "/templates/mapper.xml.ftl";
        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
                return projectPath + "/src/main/resources/mappers/" +
                    tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
            }
        });
    
        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);
    
        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();
        templateConfig.setXml(null); //是否在mapper接口处生成xml文件
        mpg.setTemplate(templateConfig); //设置模板引擎
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
    
        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel); //Entity文件名称命名规范
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);//Entity字段名称命名规范
        strategy.setEntityLombokModel(true);  //是否使用lombok完成Entity实体标注
        strategy.setRestControllerStyle(true);  //Controller注解使用是否RestController标注
        strategy.setControllerMappingHyphenStyle(true); //Controller注解名称,使用连字符(—)
        strategy.setInclude("sys_user","sys_role","sys_permission"); //要生成的表名,不写默认所有
        //strategy.setTablePrefix("sys_");//表前缀,添加该表示,则生成的实体,不会有表前缀
        //strategy.setFieldPrefix("sys_");  //字段前缀
        mpg.setStrategy(strategy);
        mpg.execute();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74

    SpringSecurity + MP 运行环境

    <context:property-placeholder location="classpath:druid.properties" />
    
    <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource"  destroy-method="close">
        <property name="driverClassName" value="${driver}" />
        <property name="url" value="${url}" />
        <property name="username" value="${user}" />
        <property name="password" value="${pwd}" />
    
        <property name="initialSize" value="${initSize}" />
        <property name="maxWait" value="${maxWait}" />
        <property name="maxActive" value="${maxSize}" />
        <property name="minIdle" value="${minSize}" />
    bean>
    
    <bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
        <property name="dataSource" ref="druidDataSource" />
        <property name="typeAliasesPackage" value="org.neuedu.spring.security.demo.entity" />
        <property name="mapperLocations" value="classpath:mappers/*Mapper.xml" />
        <property name="plugins">
            <bean class="com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor" />
        property>
    bean>
    
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="org.neuedu.spring.security.demo.mapper" />
    bean>
    
    <context:component-scan base-package="org.neuedu.spring.security.demo.service.impl" />
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    测试数据录入

    controller 中添加一个 list 请求方法,测试整合端是否 OK。

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @JsonInclude(JsonInclude.Include.NON_NULL)
    public class ResponseEntity {
        private int code;
        private String msg;
        private Long count;
        private Object data;
    
        public static ResponseEntity ok(){
            return new ResponseEntity(0,null,null,null);
        }
        public static ResponseEntity ok(String msg){
            return new ResponseEntity(0,msg,null,null);
        }
        public static ResponseEntity error(String msg){
            return new ResponseEntity(1,msg,null,null);
        }
    
        public static ResponseEntity data(Object obj){
            return new ResponseEntity(0,null,null,obj);
        }
    
        public static ResponseEntity page(long count,Object obj){
            return new ResponseEntity(0,null,count,obj);
        }
    
        public static boolean isSuccess(ResponseEntity responseEntity){
            return responseEntity.getCode() == 1;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    动态认证逻辑替换

    在角色Mapper中添加一个查询角色的方法

    public interface SysRoleMapper extends BaseMapper<SysRole> {
    
        @Select(" select r.id,r.role_name,r.role_desc from sys_user u " +
                " join sys_user_role ur on u.id = ur.UID " +
                " join sys_role r on r.id = ur.RID " +
                " where u.id = #{userId}  ")
        List<SysRole> findRoldByUserId(Integer userId);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    动态权限认证更改

    修改 SysUserServiceImp 类,实现UserDetailsService 接口,重写 loadUserByUsername 方法,完成认证逻辑,动态替换认证数据:

    @Service
    public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService, UserDetailsService {
    
        @Autowired
        private SysRoleMapper roleMapper;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
            //根据用户名获取对应的用户信息  User
            SysUser user = new LambdaQueryChainWrapper<SysUser>(baseMapper).eq(SysUser::getUsername,username).one();
            if(Objects.isNull(user)){
                return null;
            }
            //获取权限集合  SimpleGrantedAuthority
            List<SysRole> roles = roleMapper.findRoldByUserId(user.getId());
    
            //User
            Set<SimpleGrantedAuthority> authorities = new HashSet<>();
            roles.forEach(role-> authorities.add(new SimpleGrantedAuthority(role.getRoleName())));
    
            return new User(user.getUsername(),user.getPassword(),user.getStatus()==1,true,true,true,authorities);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    修改 application-security.xml 中的认证逻辑引用

    
    <security:authentication-manager>
        <security:authentication-provider user-service-ref="sysUserServiceImpl" />
    security:authentication-manager>
    
    • 1
    • 2
    • 3
    • 4

    到此完毕,但是,在认证逻辑中我们都是手动组装数据,这样比较麻烦,有没有办法可以简化呢?

    接下来,我们只需要把 SysUser 类变成 UserDetails的子类 , 把 SysRole 类变成 GrantedAuthority 的子类是不是就可以了呢?

    使用Java多态性,简化代码

    数据库实体关系

    @Data
    @EqualsAndHashCode(callSuper = false)
    @Accessors(chain = true)
    @TableName("sys_role")
    public class Role implements Serializable, GrantedAuthority {
    
        private static final long serialVersionUID=1L;
    
        // 编号
        @TableId(value = "ID", type = IdType.AUTO)
        private Integer id;
    
        // 角色名称
        @TableField("ROLE_NAME")
        private String roleName;
    
        //角色描述
        @TableField("ROLE_DESC")
        private String roleDesc;
    
    
        @Override
        public String getAuthority() {
            //返回用于认证的角色描述信息 ROLE_ADMIN,ROLE_USER 这类用于判断的对应字段
            return this.roleName;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    @Data
    @EqualsAndHashCode(callSuper = false)
    @Accessors(chain = true)
    @TableName("sys_user")
    public class User implements Serializable, UserDetails {
    
    private static final long serialVersionUID=1L;
    
        @TableId(value = "id", type = IdType.AUTO)
        private Integer id;
    
        //用户名称
        private String username;
    
        //密码
        private String password;
    
        //1开启0关闭
        private Integer status;
    
        //拥有的所有角色
        @TableField(exist = false) //数据库中不存在该字段,使用注解排除
        private List<Role> roles = new ArrayList<>();
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            //返回当前用户认证角色集合
            return roles;
        }
    
        //账户是否失效
        @Override
        public boolean isAccountNonExpired() {return true;}
    
        //账户是否锁定
        @Override
        public boolean isAccountNonLocked() {return true;}
    
        //认证是否过期
        @Override
        public boolean isCredentialsNonExpired() {return true;}
    
        //是否可用,使用数据库的用户状态来进行动态处理
        @Override
        public boolean isEnabled() {return this.getStatus()==1;}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    数据库 RoleMapper 认证方法实现

    public interface RoleMapper extends BaseMapper<Role> {
    
        @Select("SELECT r.ID,r.ROLE_NAME,r.ROLE_DESC FROM sys_user u " +
                " join sys_user_role ur on u.id = ur.UID " +
                " join sys_role r on r.ID = ur.RID " +
                " where u.id = #{userId} ")
        List<Role> findByUserId(Integer userId);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    public interface UserMapper extends BaseMapper<User> {
    
        //注意:一定要提供一个 coloum = "查询字段" , 否则 MP 会直接去找属性叫 roles 的字段,直接报错
        @Results({
            @Result(id = true,property = "id",column = "id"),
            @Result(property = "roles",column = "id",many = @Many(select =              "org.neuedu.security.demo.mapper.RoleMapper.findByUserId"))
        })
        @Select("select u.id,u.username,u.password,u.status from sys_user u where u.username = #{username} ")
        User findByName(String username);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    具体认证逻辑实现

    @Slf4j
    @Service("userService")
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService, UserDetailsService {
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User user = baseMapper.findByName(username);
            log.info("登录用户信息:{}",user);
            log.info("登录用户拥有的认证角色:{}",user.getAuthorities());
            return user;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    指定认证使用的业务对象

    
    <security:authentication-manager>
        <security:authentication-provider user-service-ref="userService">
        security:authentication-provider>
    security:authentication-manager>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    到此,大功告成,可以到页面是测试数据库动态权限,是否OK。

    密码加密与记住我功能

    加密认证功能实现

    SpringSecurity 提供了很多种密码加密的形式,而我们之前为了简单,我们使用了明文不加密的形式登录,现在我们来看看它具体的加密形式怎么使用,先修改数据库中用户的密码,去掉 {noop} 改成指定加密方式的密码:

    
    <bean id="passwordEncoder"
          class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
    
    
    <security:authentication-manager>
        <security:authentication-provider user-service-ref="userServiceImpl">
            
            <security:password-encoder ref="passwordEncoder"/>
        security:authentication-provider>
    security:authentication-manager>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这里去掉 {noop},密码需要加密后在入库,否则密码不匹配。

    @Test
    public void test(){
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String admin = passwordEncoder.encode("admin");
        String user = passwordEncoder.encode("user");
        System.out.println(admin);
        //$2a$10$GWHwW0.oU1FRlyAgR3ZMyuE1SlRlzPMfYktNG56n4oPnQCmm9Rpg.
        System.out.println(user);
        //$2a$10$SpT/iNJh3jYTbTFZEpXl8OFd60Hc18SmRU7OmwhWh93CdvvIW2G4C
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    记住我功能

    remember me 功能,到 AbstractRememberMeServices 类中查看 loginSuccess 方法:登录的时候我们传递一个参数 remember-me ,如果它的值为 trueonyes1 其中一个,则表示页面勾选了记住我选项了。具体业务逻辑由 PersistentTokenBasedRememberMeServices 完成。在这里还得注意需要开启记住我功能的过滤器。注意:验证方式不能使用 isFullyAuthenticated() , 否则记住我这个功能无法成功

    在这里插入图片描述

    
    <security:http auto-config="true" use-expressions="true">
        
    <security:remember-me data-source-ref="dataSource" 
                          token-validity-seconds="60"
    					  remember-me-parameter="remember-me"/>
    security:http>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    CREATE TABLE `persistent_logins` (
        `username` varchar(64) NOT NULL,
        `series` varchar(64) NOT NULL,
        `token` varchar(64) NOT NULL,
        `last_used` timestamp NOT NULL,
        PRIMARY KEY (`series`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    注意这张表的名称和字段都是固定的,不要修改。在我们完成认证的时候,该数据库会有相应的记录来存储记住我的 cookie 值

    <security:remember-me token-validity-seconds="600" 
                       token-repository-ref="jdbcTokenRepository" />
    
    <bean id="jdbcTokenRepository" class="org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl">
     <property name="dataSource" ref="druidDataSource" />
     
     <property name="createTableOnStartup" value="true" />
    bean>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    用户资源动态授权

    控制每一个前端请求的权限,配置如下:

    
    <security:http pattern="/favicon.ico" security="none" />
    <security:http pattern="/failure.jsp" security="none" />
    <security:http pattern="/login.jsp" security="none" />
    
    <security:http auto-config="true" use-expressions="true">
        <security:intercept-url pattern="/dept/add" access="hasRole('ROLE_ADMIN')" />
        <security:intercept-url pattern="/dept/del" access="hasRole('ROLE_ADMIN')" />
        <security:intercept-url pattern="/dept/edit" access="hasRole('ROLE_USER')" />
        <security:intercept-url pattern="/dept/list" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')" />
        <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')" />
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    前端页面控制

    Spring Security 也有对 Jsp 标签支持的标签库。其中一共定义了三个标签:authorizeauthenticationaccesscontrollist(不常用)。其中 authentication 标签是用来代表当前 Authentication对象的,我们可以利用它来展示当前 Authentication 对象的相关信息。另外两个标签是用于权限控制的,可以利用它们来包裹需要保护的内容,通常是超链接和按钮。

    • authorize : 是用来判断普通权限的,通过判断用户是否具有对应的权限而控制其所包含内容的显示,其可以指定如下属性。
      • access : 需要使用表达式来判断权限,当表达式的返回结果为true时表示拥有对应的权限
        需要注意的是因为access属性是使用表达式的,需要设置http元素的use-expressions="true"存储。
      • ifAllGranted : 不能使用表达式,由逗号分隔的权限列表,用户必须拥有所有列出的权限时显示
      • ifAnyGranted : 不能使用表达式,用户必须至少拥有其中的一个权限时才能显示
      • ifNotGranted : 不能使用表达式,用户未拥有所有列出的权限时才能显示
    • authentication : 用来代表当前 Authentication 对象,主要用于获取当前 Authentication 的相关信息
      • property : 主要属性,我们可以通过它来获取当前 Authentication 对象的相关信息
      • scope : 定义var存在的范围,默认是 pageContext
      • var : 定义一个变量 , 将其以指定的属性名进行存放,默认是存放在 pageConext
      • htmlScape : 是否需要将 html 进行转义。默认为 true
    <dependency>
        <groupId>org.springframework.securitygroupId>
        <artifactId>spring-security-taglibsartifactId>
        <version>5.1.5.RELEASEversion>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    <%@taglib uri="http://www.springframework.org/security/tags" prefix="security" %>
        
    <security:authorize access="hasAnyRole('ROLE_ADMIN')">
        <p><a href="dept/add">部门添加a>p>
    security:authorize>
    
    
    <sec:authorize ifAllGranted="ROLE_ADMIN">
        <a href="admin.jsp">admina>
    sec:authorize>
    
    
    <sec:authorize ifAnyGranted="ROLE_USER,ROLE_ADMIN">hellosec:authorize>
        
    
    <sec:authorize ifNotGranted="ROLE_ADMIN">
        <a href="user.jsp">usera>
    sec:authorize>
    
    
    欢迎你:<security:authentication property="principal.username" />
    或者
    欢迎你:<security:authentication property="name" />
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    SpringSecurity 可以通过注解的方式来控制类或者方法的访问权限。注解需要对应的注解支持,若注解放在
    controller 类中,对应注解支持应该放在 mvc 配置文件中,因为 controller类是有 mvc 配置文件扫描并创建的,同理,注解放在 service 类中,对应注解支持应该放在 spring 配置文件中。由于我们现在是模拟业务操作,并没有 service 业务代码,所以就把注解放在 controller类中了。

    服务端方法级权限控制

    在服务的我们可以通过 SpringSecurity 提供的注解对方法来进行权限控制,SpringSecurity 在方法的权限控制上支持三种注解: JSR-250注解 , @Secured注解 , 支持表达式的注解 。这三种注解默认是没有开启的,需要单独通过 global-method-security 元素对应的属性进行启用

    
    <security:global-method-security jsr250-annotations="enabled" />
    <security:global-method-security secured-annotations="enabled" />
    <security:global-method-security pre-post-annotations="enabled" />
    
    • 1
    • 2
    • 3
    • 4

    也可通过注解开启 , 需要在继承 WebSecurityConfigurerAdapter 类上加@EnableGlobalMethodSecurity 注解,并在该类中将 AthenticationManager 定义为 Bean .

    JSR-250 注解使用

    • @RolesAllowed({"USER","ADMIN"}) 具有两种权限中的一种,就可以访问。这里可以省略前缀 ROLE_

    • @PermitAll 表示允许所有的角色进行访问,也就是说不进行权限控制

    • @DenyAll 表示无论什么角色都不可以访问,与 @PermitAll 相反

    @Secured注解

    JSR-250注解 使用一致,只是这个注解是 SpringSecurity 默认提供的,使用的时候不用额外引入 坐标 ,还有一点就是这个注解的角色需要加上前缀 ROLE_

    支持 SPEL 表达式的注解

    • @PreAuthorize("hasRole('ADMIN')") 在方法调用之前,基于表达式的计算结果来限制对方法的访问
    • @PostAuthorize 允许方法调用,但是如果表达式计算结果为 false ,将抛出一个安全性异常
    • @PostFilter 允许方法调用,但必须按照表达式来过滤方法的结果
    • @PreFilter 允许方法调用,但必须在进入方法之前过滤输入值

    案例演示

    
    <security:global-method-security pre-post-annotations="enabled" />
    
    • 1
    • 2
    在注解支持对应类或者方法上添加注解
    @RestController
    @RequestMapping("/dept")
    public class DeptController {
    
        @Autowired
        private ISysUserService userService;
    
        @PreAuthorize("hasRole('ROLE_ADMIN')")
        @GetMapping("/add")
        public String add(){
            return "dept add  .....  ";
        }
    
        @PreAuthorize("hasRole('ROLE_ADMIN') and #id==5 ")
        @GetMapping("/del")
        public String del(Integer id){
            return id+"dept del  .....  ";
        }
    
        @PreAuthorize("hasRole('ROLE_USER')")
        @GetMapping("/edit")
        public String edit(){
            return "dept edit  .....  ";
        }
    
        @PostAuthorize("returnObject.username.equals('admin')")
        @GetMapping("/list")
        public SysUser list(Integer id){
             return userService.getById(id);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    权限不足异常处理 - 编写异常处理器
    @ControllerAdvice
    public class ControllerExceptionAdvice {
        //只有出现AccessDeniedException异常才调转403.jsp页面
        @ExceptionHandler(AccessDeniedException.class)
        public String exceptionAdvice(){
            return "forward:/403.jsp";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    整合 SpringBoot

    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.1.3.RELEASEversion>
        <relativePath/>
    parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>
    dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    @RestController
    @RequestMapping("/product")
    public class ProductController {
        
        @RequestMapping
        public String hello(){
            return "success";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    SpringBoot 已经为 SpringSecurity 提供了默认配置,默认所有资源都必须认证通过才能访问,那么问题来了!此刻并没有连接数据库,也并未在内存中指定认证用户,如何认证呢?其实SpringBoot已经提供了默认用户名 user ,密码在项目启动时随机生成,在日志中可以查看到:

    在这里插入图片描述

    整合 Jsp 模板

    SpringBoot 官方是不推荐在 SpringBoot 中使用 jsp 的,那么到底可以使用吗?答案是肯定的!
    不过需要导入 tomcat 插件启动项目,不能再用 SpringBoot 默认 tomcat 了。

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-tomcatartifactId>
    dependency>
    <dependency>
        <groupId>org.apache.tomcat.embedgroupId>
        <artifactId>tomcat-embed-jasperartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    核心配置文件中配置视图解析器

    spring.mvc.view.prefix=/pages/
    spring.mvc.view.suffix=.jsp
    
    • 1
    • 2

    提供 SpringSecurity 配置类

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        //先不连接数据库,提供静态用户名和密码
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                .withUser("user")
                .password("{noop}123")
                .roles("USER");
        }
        
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                .antMatchers("/login.jsp", "/failer.jsp", "/css/**", "/img/**",
                             "/plugins/**").permitAll()
                .antMatchers("/**").hasAnyRole("USER")
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .loginPage("/login.jsp")
                .loginProcessingUrl("/login")
                .successForwardUrl("/index.jsp")
                .failureForwardUrl("/failer.jsp")
                .permitAll()
                .and()
                .logout()
                .logoutUrl("/logout")
                .invalidateHttpSession(true)
                .logoutSuccessUrl("/login.jsp")
                .permitAll()
                .and()
                .csrf()
                .disable();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    数据库认证

    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
        <version>5.1.47version>
    dependency>
    <dependency>
        <groupId>com.baomidougroupId>
        <artifactId>mybatis-plus-boot-starterartifactId>
        <version>3.2.0version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.url=jdbc:mysql:///security_authority
    spring.datasource.username=root
    spring.datasource.password=root
    
    logging.level.org.neuedu=debug
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    创建角色 pojo 对象 - 这里直接使用 SpringSecurity 的角色规范

    @Data
    public class SysRole implements GrantedAuthority {
        private Integer id;
        private String roleName;
        private String roleDesc;
        
        //标记此属性不做json处理
        @JsonIgnore
        @Override
        public String getAuthority() {
            return roleName;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    创建用户 pojo 对象,这里直接实现 SpringSecurity 的用户对象接口,并添加角色集合私有属性。

    @Data
    public class SysUser implements UserDetails {
        private Integer id;
        private String username;
        private String password;
        private Integer status;
        private List<SysRole> roles = new ArrayList<>();
        
        @JsonIgnore
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return roles;
        }
        
        @JsonIgnore
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
        
        @JsonIgnore
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
        
        @JsonIgnore
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
        
        @JsonIgnore
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    提供角色 mapper 接口

    public interface RoleMapper extends Mapper<SysRole> {
        @Select("SELECT r.id, r.role_name roleName, r.role_desc roleDesc " +
                "FROM sys_role r, sys_user_role ur " +
                "WHERE r.id=ur.rid AND ur.uid=#{uid}")
        public List<SysRole> findByUid(Integer uid);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    提供用户mapper接口

    public interface UserMapper extends Mapper<SysUser> {
        @Select("select * from sys_user where username=#{username}")
        @Results({
            @Result(id = true, property = "id", column = "id"),
            @Result(property = "roles", column = "id", javaType = List.class,
                    many = @Many(select = "com.itheima.mapper.RoleMapper.findByUid"))
        })
        public SysUser findByUsername(String username);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    提供认证 service 接口

    import org.springframework.security.core.userdetails.UserDetailsService;
    
    public interface UserService extends UserDetailsService {
    }
    
    • 1
    • 2
    • 3
    • 4

    提供认证service实现类

    @Service
    @Transactional
    public class UserServiceImpl implements UserService {
        
        @Autowired
        private UserMapper userMapper;
        
        @Override
        public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
            return userMapper.findByUsername(s);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    启动类中加入 Mapper 接口扫描 以及 密码加密 Bean 对象

    @SpringBootApplication
    @MapperScan("org.neuedu.security.mapper")
    public class Application {
    
        public static void main(String[] args) {
            SpringApplication.run(QuickStartApplication.class, args);
        }
        
        @Bean
        public BCryptPasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    修改配置类

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        
        @Autowired
        private UserService userService;
        
        @Autowired
        private BCryptPasswordEncoder passwordEncoder;
        
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userService).passwordEncoder(passwordEncoder);
        }
        
        protected void configure(HttpSecurity http) throws Exception {
            http
                允许不登陆就可以访问的方法,多个用逗号分隔
                .authorizeRequests()
                .antMatchers("/login.jsp", "/failer.jsp", "/css/**", "/img/**",
                             "/plugins/**").permitAll()
                .antMatchers("/**").hasAnyRole("USER")
                //其他的需要授权后访问
                .anyRequest()
                .authenticated()
                .and()
                //表单登录
                .formLogin()
                .loginPage("/login.jsp")
                .loginProcessingUrl("/login")
                .successForwardUrl("/index.jsp")
                .failureForwardUrl("/failer.jsp")
                .permitAll()
                .and()
                //退出
                .logout()
                .logoutUrl("/logout")
                .invalidateHttpSession(true)
                .logoutSuccessUrl("/login.jsp")
                .permitAll()
                .and()
                //关闭 csrf
                .csrf()
                .disable();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    整合实现授权功能

    在启动类上添加开启方法级的授权注解 @EnableGlobalMethodSecurity(prePostEnabled= true)

    在产品处理器类上添加注解 @PreAuthorize('ADMIN')

    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/test")
    public String test(){
        return "info";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    指定自定义异常页面

    编写异常处理器拦截403异常

    @ControllerAdvice
    public class HandleControllerException {
        
        @ExceptionHandler(RuntimeException.class)
        public String exceptionHandler(RuntimeException e){
            if(e instanceof AccessDeniedException){
                //如果是权限不足异常,则跳转到权限不足页面!
                return "redirect:/403.jsp";
            }
            //其余的异常都到500页面!
            return "redirect:/500.jsp";
        }
        
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    整合Thymeleaf

    认证

    使用默认用户名 user 与 控制台生成的随机密码进行登录

    #修改配置文件,自定义用户名和密码
    spring.security.user.name=admin
    spring.security.user.password=admin
    
    • 1
    • 2
    • 3
    //继承 WebSecurityConfigurerAdapter 重写 configure(auth) 方法,代码指定用户名和密码
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        //认证相关  用户名密码
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //基于内存
            auth.inMemoryAuthentication()
                .withUser("admin").password("{noop}admin").roles("ROLE_ADMIN")
                .and()
                .withUser("user").password("{noop}user").roles("ROLE_USER");
            //基于数据库
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    授权

    //继承 WebSecurityConfigurerAdapter 重写 configure(http) 方法,完成授权操作
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/hello").hasAnyRole("ADMIN")
            .anyRequest().authenticated();
    
        //表单登录
        http.formLogin()
     
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    自定义登录页面

    //授权,请求相关
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/logPage").permitAll()
            .antMatchers("/hello").hasAnyRole("ADMIN")
            .anyRequest().authenticated();
    
        //表单的配置
        http.formLogin()
            .usernameParameter("logName").passwordParameter("logPwd")
            .loginPage("/logPage").loginProcessingUrl("/login");
    
        //csrf
        http.csrf().disable();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    记住我功能

    //授权,请求相关
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/logPage").permitAll()
            .antMatchers("/hello").hasAnyRole("ADMIN")
            .anyRequest().authenticated();
    
        //表单的配置
        http.formLogin()
            .usernameParameter("logName").passwordParameter("logPwd")
            .loginPage("/logPage").loginProcessingUrl("/login");
    
        //记住我
        http.rememberMe().rememberMeParameter("forgetMe").rememberMeCookieName("forgetMe")
            .tokenValiditySeconds(5);
        
        //csrf
        http.csrf().disable();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    基于数据库认证

    • 角色名称需要注意:加上 ROLE_ 前缀,做判断的时候,可以不用省略
    • 用户登录密码:如果是明文登录需要加上{noop} 前缀,否则需要生成加密密码在存储到数据表中
    -- ----------------------------
    -- Table structure for sys_authority
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_authority`;
    CREATE TABLE `sys_authority`  (
      `id` int(11) NOT NULL COMMENT '主键编号',
      `authority_name` varchar(25)  COMMENT '权限名称',
      `authority_url` varchar(25)  COMMENT '权限地址',
      `icon` varchar(25)  COMMENT '图标',
      `parent_id` int(11) DEFAULT NULL COMMENT '上级模块',
      `permission` varchar(255)  COMMENT '权限值',
      `sort_num` int(3) DEFAULT NULL COMMENT '排序号',
      `remark` varchar(200)  COMMENT '备注',
      `create_time` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
      PRIMARY KEY (`id`) USING BTREE
    ) COMMENT = '权限';
    
    -- ----------------------------
    -- Records of sys_authority
    -- ----------------------------
    INSERT INTO `sys_authority` VALUES (1, '用户管理', '/user/list', NULL, NULL, 'user:add,user:del', NULL, NULL, '2020-01-03 14:14:06');
    
    -- ----------------------------
    -- Table structure for sys_role
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_role`;
    CREATE TABLE `sys_role`  (
      `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键编号',
      `role_name` varchar(25)  COMMENT '角色名称',
      `remark` varchar(200)  COMMENT '备注',
      `create_time` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
      PRIMARY KEY (`id`) USING BTREE
    ) COMMENT = '角色';
    
    -- ----------------------------
    -- Records of sys_role
    -- ----------------------------
    INSERT INTO `sys_role` VALUES (1, 'ROLE_ADMIN', '系统管理员', '2020-01-03 14:12:47');
    INSERT INTO `sys_role` VALUES (2, 'ROLE_MGR', '销售主管', '2020-01-03 14:12:51');
    
    -- ----------------------------
    -- Table structure for sys_role_authority
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_role_authority`;
    CREATE TABLE `sys_role_authority`  (
      `id` int(11) NOT NULL COMMENT '主键编号',
      `authority_id` int(11) DEFAULT NULL COMMENT '权限ID',
      `role_id` int(11) DEFAULT NULL COMMENT '角色ID',
      `remark` varchar(200)  COMMENT '备注',
      `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
      PRIMARY KEY (`id`) USING BTREE
    ) COMMENT = '角色-权限关系表';
    
    -- ----------------------------
    -- Table structure for sys_user
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_user`;
    CREATE TABLE `sys_user`  (
      `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键编号',
      `real_name` varchar(25)  COMMENT '真实姓名',
      `log_name` varchar(25)  COMMENT '登录名',
      `log_pwd` varchar(64)  COMMENT '密码',
      `gender` int(12) DEFAULT NULL COMMENT '性别,0 女,1男',
      `disabled` int(255) DEFAULT 1 COMMENT '是否禁用,1启用,0禁用',
      `remark` varchar(200)  COMMENT '备注',
      `create_time` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
      PRIMARY KEY (`id`) USING BTREE
    ) COMMENT = '用户';
    
    -- ----------------------------
    -- Records of sys_user
    -- ----------------------------
    INSERT INTO `sys_user` VALUES (1, '刘颖', 'admin', 'admin', 0, 1, NULL, '2020-01-03 14:11:32');
    
    -- ----------------------------
    -- Table structure for sys_user_role
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_user_role`;
    CREATE TABLE `sys_user_role`  (
      `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键编号',
      `user_id` int(11) DEFAULT NULL COMMENT '用户ID',
      `role_id` int(11) DEFAULT NULL COMMENT '角色ID',
      `remark` varchar(200)  COMMENT '备注',
      `create_time` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
      PRIMARY KEY (`id`) 
    ) COMMENT = '用户角色关系表';
    
    -- ----------------------------
    -- Records of sys_user_role
    -- ----------------------------
    INSERT INTO `sys_user_role` VALUES (1, 1, 1, NULL, '2019-12-14 15:14:20');
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.url=jdbc:mysql:///crm_system?serverTimezone=UTC
    spring.datasource.username=root
    spring.datasource.password=root
    
    spring.datasource.druid.initial-size=10
    spring.datasource.druid.max-active=50
    
    spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
    spring.jackson.default-property-inclusion=non_null
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    @Configuration
    public class LocalDateTimeSerializerConfig {
    
        @Value("${spring.jackson.date-format}")
        private String pattern;
    
        @Bean
        public LocalDateTimeSerializer localDateTimeSerializer() {
            return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(pattern));
        }
    
        @Bean
        public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer(
            LocalDateTimeSerializer localDateTimeSerializer
        ) {
            
            return builder -> builder.serializerByType(
                LocalDateTime.class, localDateTimeSerializer
            );
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    public interface ISysUserService extends IService<SysUser>, UserDetailsService {}
    
    @Service
    @Slf4j
    public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {
    	
        @Autowired
        private PasswordEncoder passwordEncoder;
        
        @Override
        public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
    
            //根据登录名称进行查询
            SysUser user = this.lambdaQuery().eq(SysUser::getLogName, logName).one();
    
            //根据用户ID查询对应的角色
            List<SysRole> roleList = roleMapper.selectRoleByUserId(user.getId());
            
            //组装权限对象
            List<GrantedAuthority> authorities = new ArrayList<>();
            roleList.forEach(role->{
                authorities.add(new SimpleGrantedAuthority(role.getRoleName()))
            });
    
            //组装用户对象
            return new User(
                user.getLogName(),
                passwordEncoder.encode(user.getLogPwd()),
                authorities
            );
            
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
    //认证相关  用户名密码
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //基于数据库
        auth.userDetailsService(userService).passwordEncoder(this.passwordEncoder());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    简化认证组装操作

    • Sys_user 实现 UserDetails 接口,指定用户名,密码,角色集合以及账号状态
    • SysRole 实现 GrantedAuthority 接口,指定角色组装的名称
    • SysRoleMapper 接口中添加一个方法 selectRoleByUserId 用于角色集合查询
    • SysUserMapper 接口中添加一个方法 selectUserByLogName 用于登录查询
    public interface SysRoleMapper extends BaseMapper<SysRole> {
    
        @Select(" select id,role_name,remark,create_time from sys_user u " +
                " join sys_user_role ur on u.id = ur.user_id " +
                " join sys_role r on r.id = ur.role_id " +
                " where u.id = #{id} ")
        List<SysRole> selectRoleByUserId(Integer id);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    public interface SysUserMapper extends BaseMapper<SysUser> {
    
        @Results({
            @Result(id = true,property = "id",column = "id"),
            @Result(property = "roles",column = "id",javaType = List.class,
                    many = @Many(select = "org.neuedu.security.mapper.SysRoleMapper.selectRoleByUserId") )
        })
        @Select("select id,real_name,log_name,log_pwd,gender,disabled,remark,create_time from sys_user where log_name =#{logName}  ")
        SysUser selectUserByLogName(String logName);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    @Service
    @Slf4j
    public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {
    
        @Override
        public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
            return baseMapper.selectUserByLogName(name);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    开启方法级权限控制

    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
         @Autowired
        private ISysUserService userService;
    
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
        //认证相关  用户名密码
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //基于数据库
            auth.userDetailsService(userService).passwordEncoder(this.passwordEncoder());
        }
    
        //web资源,静态资源的配置
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/logPage"); //登录请求不加入 security 中
        }
    
        //授权,请求相关
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .anyRequest().authenticated();
            //表单的配置
            http.formLogin()
                    .usernameParameter("logName").passwordParameter("logPwd")
                	.loginPage("/logPage").loginProcessingUrl("/login");
    
            //记住我功能
            http.rememberMe().rememberMeParameter("forgetMe")
                .rememberMeCookieName("forgetMe").tokenValiditySeconds(10);
            //csrf
            http.csrf().disable();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    @RestController
    @RequestMapping("/sysUser")
    public class SysUserController {
    
        @Autowired
        private ISysUserService userService ;
    
        @PreAuthorize("hasRole('USER')")
        @GetMapping("/list")
        public ResponseEntity list(){
            return ResponseEntity.data(userService.list());
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    具有权限完成 按钮级别的权限认证

    <mapper namespace="org.neuedu.security.mapper.SysAuthorityMapper">
    
        <select id="selectByUserId" resultType="SysAuthority">
             SELECT distinct a.* FROM sys_authority a
             join sys_role_authority ra on ra.authority_id = a.id
             join sys_role r on r.id = ra.role_id
             join sys_user_role ur on ur.role_id = r.id
             join sys_user u on u.id = ur.user_id
             where u.id = #{userId}
             <if test="type != null">
                 and type = #{type}
             if>
             order by a.id
        select>
    
    mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    @Service
    @Slf4j
    public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {
    
        @Autowired
        private SysAuthorityMapper authorityMapper;
    
        @Override
        public UserDetails loadUserByUsername(String logName) throws UsernameNotFoundException {
            //1. 根据用户名去登录
            SysUser currentUser = this.lambdaQuery().eq(SysUser::getLogName, logName).one();
            if(Objects.isNull(currentUser)){
                throw new UsernameNotFoundException("用户名或密码输入有误~");
            }
            //2. 查询权限  type = 1 , 0
            List<SysAuthority> authorities = authorityMapper.selectByUserId(currentUser.getId(), null);
            currentUser.setAuthorities(authorities);
            return currentUser;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    @PreAuthorize("hasAuthority('user:list')")
    @GetMapping("/list")
    @ResponseBody
    public ResponseEntity list(){
        return ResponseEntity.data(userService.list());
    }
    
    @PreAuthorize("hasAuthority('user:del')")
    @GetMapping("/del")
    public @ResponseBody ResponseEntity del(){
        return ResponseEntity.ok("删除");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    前后端分离

    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private ISysUserService userService;
    
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
        //认证相关  用户名密码
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //基于数据库
            auth.userDetailsService(userService).passwordEncoder(this.passwordEncoder());
        }
    
        //web资源,静态资源的配置
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/sysUser/logPage");
        }
    
        //授权,请求相关
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            http.authorizeRequests()
                    .anyRequest().authenticated();
            //表单的配置
            http.formLogin()
                    .usernameParameter("logName").passwordParameter("logPwd").loginProcessingUrl("/login")
                    .successHandler((httpServletRequest, httpServletResponse, authentication) -> {
                        ResponseEntity responseEntity = ResponseEntity.ok("认证成功!");
                        responseJSON(httpServletResponse, responseEntity);
                    })
                    .failureHandler((httpServletRequest, httpServletResponse, e) -> {
                        ResponseEntity responseEntity = null;
                        if(e instanceof UsernameNotFoundException || e instanceof BadCredentialsException){
                            responseEntity = ResponseEntity.exception(ResponseCode.USERNAME_PASSWORD_EXCEPTION);
                        }else if(e instanceof DisabledException){
                            responseEntity = ResponseEntity.exception(ResponseCode.ACCOUNT_DISABLED);
                        }else{
                            responseEntity = ResponseEntity.exception(ResponseCode.SYSTEM_EXCEPTION);
                        }
                        responseJSON(httpServletResponse,responseEntity);
                    })
                    .and()
                    .exceptionHandling()
                    .authenticationEntryPoint((httpServletRequest, httpServletResponse, e) -> {
                        ResponseEntity responseEntity = ResponseEntity.exception(ResponseCode.NEED_LOGIN);
                        responseJSON(httpServletResponse,responseEntity);
                    })
                    .accessDeniedHandler((httpServletRequest, httpServletResponse, e) -> {
                        ResponseEntity responseEntity = ResponseEntity.exception(ResponseCode.AUTHORIZE_EXCEPTION);
                        responseJSON(httpServletResponse,responseEntity);
                    });
            //注销
            http.logout()
                    .logoutUrl("/logout").invalidateHttpSession(true)
                    .logoutSuccessHandler((httpServletRequest, httpServletResponse, authentication) -> {
                        ResponseEntity responseEntity = ResponseEntity.ok("注销成功~");
                        responseJSON(httpServletResponse,responseEntity);
                    });
    
            //记住我功能
            http.rememberMe().rememberMeParameter("forgetMe").rememberMeCookieName("forgetMe").tokenValiditySeconds(10);
            //csrf
            http.csrf().disable();
        }
    
    
        private void responseJSON(HttpServletResponse httpServletResponse, ResponseEntity responseEntity) throws IOException {
            httpServletResponse.setContentType("application/json;charset=utf-8");
            PrintWriter out = httpServletResponse.getWriter();
            try {
                out.write(JsonUtil.ToStringIgnoreNull(responseEntity));
            } catch (Exception e) {
                System.out.println("JSON序列化错误");
            }
            out.close();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86

    前后端分离,提取认证对象

    @Component
    public class SecurityAuthorizeHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler, AccessDeniedHandler, AuthenticationEntryPoint, LogoutSuccessHandler {
    
        private  ResponseEntity responseEntity;
    
        //认证成功
        @Override
        public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
            responseEntity = ResponseEntity.ok("认证成功!");
            responseJSON(httpServletResponse, responseEntity);
        }
    
        //认证失败
        @Override
        public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
            if(e instanceof UsernameNotFoundException || e instanceof BadCredentialsException){
                responseEntity = ResponseEntity.exception(ResponseCode.USERNAME_PASSWORD_EXCEPTION);
            }else if(e instanceof DisabledException){
                responseEntity = ResponseEntity.exception(ResponseCode.ACCOUNT_DISABLED);
            }else{
                responseEntity = ResponseEntity.exception(ResponseCode.SYSTEM_EXCEPTION);
            }
            responseJSON(httpServletResponse,responseEntity);
        }
    
        //403权限不足
        @Override
        public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
            responseEntity = ResponseEntity.exception(ResponseCode.AUTHORIZE_EXCEPTION);
            responseJSON(httpServletResponse,responseEntity);
    
        }
    
        //非法访问
        @Override
        public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
            responseEntity = ResponseEntity.exception(ResponseCode.NEED_LOGIN);
            responseJSON(httpServletResponse,responseEntity);
    
        }
    
        //注销成功
        @Override
        public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
            responseEntity = ResponseEntity.ok("注销成功~");
            responseJSON(httpServletResponse,responseEntity);
        }
    
        //Response JSON 响应
        private void responseJSON(HttpServletResponse httpServletResponse, ResponseEntity responseEntity) throws IOException {
            httpServletResponse.setContentType("application/json;charset=utf-8");
            PrintWriter out = httpServletResponse.getWriter();
            try {
                out.write(JsonUtil.ToStringIgnoreNull(responseEntity));
            } catch (Exception e) {
                System.out.println("JSON序列化错误");
            }
            out.close();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private ISysUserService userService;
    
        @Autowired
        private SecurityAuthorizeHandler authorizeHandler;
    
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
        //认证相关  用户名密码
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //基于数据库
            auth.userDetailsService(userService).passwordEncoder(this.passwordEncoder());
        }
    
        //web资源,静态资源的配置
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/sysUser/logPage");
        }
    
        //授权,请求相关
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            http.authorizeRequests()
                    .anyRequest().authenticated();
            //表单的配置
            http.formLogin()
                    .usernameParameter("logName").passwordParameter("logPwd").loginProcessingUrl("/login")
                    .successHandler(authorizeHandler)
                    .failureHandler(authorizeHandler)
                    .and()
                    .exceptionHandling()
                    .authenticationEntryPoint(authorizeHandler)
                    .accessDeniedHandler(authorizeHandler);
            //注销
            http.logout()
                    .logoutUrl("/logout").invalidateHttpSession(true)
                    .logoutSuccessHandler(authorizeHandler);
    
            //记住我功能
            http.rememberMe().rememberMeParameter("forgetMe").rememberMeCookieName("forgetMe").tokenValiditySeconds(10);
            //csrf
            http.csrf().disable();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    ajax请求:

    function login(){
        $.ajax({
            method:'post',
            url:'/login',
            dataType:'text',  //需要注意返回数据类型,否则可能JSON解析失败,会进入error
            data:{logName:'user',logPwd:'user'},
            success:function (res) {
                console.log('succ');
                console.log(res);
            },
            error:function (res) {
                console.log('error');
                console.log(res);
            }
        });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
  • 相关阅读:
    windows环境下Git安装与远程仓库SSH Keys配置
    「硬核」实操如何拥有一个自己的数字人模型
    《HelloGitHub》第 83 期
    【Vue3 知识第六讲】ref、 shallowRef、unref、isRef 等系列知识应用
    vue要做权限管理该怎么做?接口权限 、按钮权限 、 菜单权限 、路由权限
    【OpenCV】直方图计算 & 均衡化直方图
    高效接口重试机制的实现
    【Java 进阶篇】深入了解 Bootstrap 组件
    LeetCode-215. 数组中的第K个最大元素-Java-medium
    Direct3D粒子系统
  • 原文地址:https://blog.csdn.net/u010158540/article/details/127889218