天天看点

springsecurity权限验证

现在是大三暑假,有机会来到企业里面学习一些东西,在接触一段 spring boot 之后需要做权限验证,只是对登录用户的进行权限验证,将整理的内容和大家分享一下。

看到的一些博客的内容:

https://blog.csdn.net/UpdateAllTheTime/article/details/82664103

https://www.cnblogs.com/rolandlee/p/9580492.html

上面的都不是 springboot.security 的内容,而是控制 mvc 的方式。

下面的是使用 springboot.security 进行的配置,这个是 2.0

https://blog.csdn.net/qq_24434671/article/details/86595157

https://blog.csdn.net/u014174854/article/details/83338804

大佬:https://www.iteye.com/blog/412887952-qq-com-2441544

最详细的信息自然:来自官方文档https://spring.io/blog/2013/07/03/spring-security-java-config-preview-web-security/#wsca

上面的博客,有的写的偏理论,实践性的内容涉及的内容不适合我现在的知识框架,有的是完全没有注释,真的让人很苦恼,下面把搜集到的信息进行整理,得到一份比较实用的内容。

首先,我们进行权限管理使用到的是 spring 框架中的 SpringSecurity 部分的内容。

什么是 SpringSecurity?

是基于 SpringAOP 和 Servlet 过滤的安全框架,在 Web 请求级和方法调用级处理身份确认和授权,还是使用的还是依赖注入和面向切面的功能。

安全主要包括两个操作 ”认证“ (为用户建立一个他所声明的主体,主体一般指的是用户、设备、系统)和 ”授权“ (或者权限控制)。

其中 Spring Security 对 Web 安全性的支持大量地依赖于 Servlet 过滤器。这些过滤器拦截进入的请求,在应用程序处理该请求之前进行某些安全处理。

FilterToBeanProxy 是一个特殊的 Servlet 处理器,它本身没有做太多的工作,而是将自己的工作委托给 Spring 应用程序上下文中的一个 Bean 完成。被委托的 Bean 和其他的 Servlet 过滤器一样,实现 javax.servlet.Filter 的接口。FilterToBeanProxy代理给的那个Bean可以是javax.servlet.Filter的任意实现。

需要添加的依赖

web 启动器和 security 启动器

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>
           

数据库的设计:

想要达到的目的是:

对不同身份的用户添加限定,指定身份的用户,只能进行指定的操作。

涉及的表:用户表(user),权限表(role),权限分配表(user_role)

各个表的内容:

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL,
  `password` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL,
  `gender` tinyint(4) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci 
           
CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `rolename` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL,
  `roledesc` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

           
CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `userId` int(11) NOT NULL,
  `roleId` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `userId` (`userId`),
  KEY `roleId` (`roleId`),
  CONSTRAINT `user_role_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`),
  CONSTRAINT `user_role_ibfk_2` FOREIGN KEY (`roleId`) REFERENCES `role` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

           

想要查询所用用户对应的权限信息使用到的 sql 语句

select u.username as name, u.password as password, r.rolename as role, r.roledesc as describute
 from user as u, role as r, user_role as ur
 where u.id=ur.userId and ur.roleId = r.id;
           

为了方便,可以用上面的 sql 语句创建了视图:

+------------+-------------+------+-----+---------+-------+
| Field      | Type        | Null | Key | Default | Extra |
+------------+-------------+------+-----+---------+-------+
| name       | varchar(20) | YES  |     | NULL    |       |
| password   | varchar(20) | YES  |     | NULL    |       |
| role       | varchar(20) | YES  |     | NULL    |       |
| describute | varchar(50) | YES  |     | NULL    |       |
+------------+-------------+------+-----+---------+-------+
           

插入的数据在视图中显示如下为:

+-------+----------+--------+------------+
| name  | password | role   | describute |
+-------+----------+--------+------------+
| admin | admin    | admin  | admin      |
| admin | admin    | normal | normal     |
| user  | 123      | admin  | admin      |
+-------+----------+--------+------------+
           

创建 springboot 的项目

完整的 pom.xml 文件,关于mybayis 部分需要导入两个依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.nianzuochen.cn</groupId>
    <artifactId>testsecurity</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>
        <!-- springsecurity 启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- spring 的 web 启动器 -->
        <!-- 不使用内置的 tomcat 改为内资的 undertow -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>

        <!-- 简化实体类方法书写 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.22</version>
            <scope>provided</scope>
        </dependency>

        <!-- 数据库连接 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.6</version>
        </dependency>

        <!--
            整合 mybatis 需要导入 mybatis-spring-boot-starter
            和 mysql-connector-java
        -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>

        <!-- swagger2 文档自动生成做测试 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.2.2</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <!-- spring 提供的插件,可进行 package,clean 等 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
        <!-- 配置扫描器查询到 .xml 文件 -->
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>
</project>
           

添加了数据库连接的启动器,所以需要添加关于数据库连接的配置:

application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/testsecurity?characterEncoding=utf8&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
           

编写启动类,启动 springboot 项目

@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
@EnableSwagger2
@MapperScan("com.nianzuochen.cn.mapper")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
           

idea的控制台会出现一个初始的密码:

springsecurity权限验证

浏览器输入:http://localhost:8080 会得到一个默认的用户登录页面:

springsecurity权限验证

可以在启动类中将这个默认的登录验证框去掉:

@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
           

接下来完成针对数据库的 mybatis 的内容

需要注意的部分:

1.UserInfo 类:

SpringSecurity 使用的是 UserDetail 作为登陆用户类,也就是用户登录的 username 和 password 会被封装进 UserDetail 类中,进行接下来的授权。

/**
 * 存储用户的详细信息,继承自 UserDetails 类,将用户的信息提交给 WebSecurityConfigurerAdapter
 * web 安全配置适配器,通过该类中提供的信息判断登录用户的权限,进行权限处理
 */

@Getter
@Setter
public class UserInfo implements UserDetails {

    private Long id;
    private String username;
    private String password;
    private Integer gender;
    private List<Role> roles;


    /**
     *  继承的方法授予的权限的内容
     * @return
     */
    @Override
    @JsonIgnore
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            // GrantedAuthority 的实现类 SimpleGrantedAuthority
            authorities.add(new SimpleGrantedAuthority(role.getRolename()));
        }
        return authorities;
    }

    @Override
    @JsonIgnore
    public String getPassword() {
       return password;
    }

    @Override
    @JsonIgnore
    public String getUsername() {
        return username;
    }

    /**
     * 表明用户的账户是否过期,设定为未过期,这里可以添加自定义的过期内容
     * 比如:在用户信息中添加登录的时间,然后设定过期时间,判断后决定是否让当前登录用户的账户过期
     * @return
     */
    @Override
    @JsonIgnore
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 表明当前用户是否被上锁
     * @return
     */
    @Override
    @JsonIgnore
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 判断当前登录用户的凭证(密码)是否过期,过期的凭证阻止身份验证
     * @return
     */
    @Override
    @JsonIgnore
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 判断当前用户是否启用
     * @return
     */
    @Override
    @JsonIgnore
    public boolean isEnabled() {
        return true;
    }
}
           

2.role 类:

Role 是 UserInfo 的一个参数,因为 UserInfo extends UserDetail ,而 UserDetail implements Serializable 所以 Role 也需要实现序列化。

还有在 SpringSecurity 中限定用户的权限名称必须以 ROLE_ 开头,在输入测试用例的时候要注意。

@Getter
@Setter
public class Role implements Serializable {

    private Long id;
    /**
     * rolename 的内容必须是以 ROLE_ 开头的内容,
     */
    private String rolename;
    private String roledesc;
}
           

3.对应的 xml 文件:

涉及到多表查询

<mapper namespace="com.nianzuochen.cn.mapper.RoleMapper">
    <select id="loadRoleByUserId" resultType="com.nianzuochen.cn.domain.Role">
        select * from role as r, user_role as ur where #{userId} = ur.userId and ur.roleId = r.id;
    </select>
</mapper>
           
<mapper namespace="com.nianzuochen.cn.mapper.UserInfoMapper">
    <!-- 使用 mybatis 将查询到的数据和 UserInfo 进行匹配 -->
    <select id="loadUserByUsername" resultMap="lazyLoadRoles" >
        select * from user WHERE username=#{username};
    </select>
    <resultMap id="lazyLoadRoles" type="com.nianzuochen.cn.dao.UserInfo">
        <id column="id" property="id"/>
        <id column="username" property="username"/>
        <id column="password" property="password"/>
        <id column="gender" property="gender"/>
        <collection property="roles" ofType="com.nianzuochen.cn.domain.Role"
                    select="com.nianzuochen.cn.mapper.RoleMapper.loadRoleByUserId" column="id">
        </collection>
    </resultMap>
</mapper>
           

4.UserInfoService 类

SpringSecurity 在使用 UserDetail 对用户信息做了限定,同时对 Service 也进行了限定。给出父类 UserDetailService,里面仅有一个根据用户名查询用户信息的抽象方法

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
           

UserInfoServiceImpl 类自然要实现这个基础方法:

@Service
// @Transactional 注解管理事务的
@Transactional
public class UserInfoServiceImpl implements UserDetailsService {


    @Autowired
    UserInfoMapper userInfoMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserInfo userInfo = userInfoMapper.loadUserByUsername(username);
        if (userInfo == null) {
            throw new UsernameNotFoundException("用户名不对");
        }

        return userInfo;
    }
}
           

5. 在 controller 中添加一个测试:

使用 swagger,也就是在浏览器中输入 http://localhost:8080/swagger-ui.html 调出 swagger 开始测试接口文档。

/**
 * 使用 admin 测得如下内容
 * 测试查询到用户的信息,在实体类中将用户的 username 和 password 进行了忽略处理
 * {"id":1,"gender":1,"roles":[{"id":1,"rolename":"ROLE_admin","roledesc":"admin"},{"id":2,"rolename":"ROLE_normal","roledesc":"normal"}]}
 * @return
 */
@GetMapping("/user")
public UserInfo getAdmin(String username) {
    return (UserInfo) userInfoService.loadUserByUsername(username);
}
           

目前位置数据采集的部分算是完成了,接下来的是使用 SpringSecurity 授权的内容。

springsecurity权限验证

最基本的安全配置信息在 WebSecurityConfigureAdapter 类中,里面有很多安全相关的配置:

//身份认证管理器创建者
protected void configure(AuthenticationManagerBuilder auth) 
// 关于 web 安全的配置
public void configure(WebSecurity web)
// 关于 http 安全的配置
protected void configure(HttpSecurity http)
......
           

在对当前登录对象进行数据匹配的时候自然要导入所有用户的信息,在 spring security 5.x 之后需要对用户的密码指定加密方式,采用 Bcypt : bcrypt 是一种跨平台的文件加密工具。

想要开启 Spring 方法级的安全,使用 @EnableGlobalMethodSecurity 注解

下面是此次权限验证 WebSecurityConfigureAdapter 的完整信息

/**
 * 此类是一个配置类
 */
@Configuration      // 声明为配置类
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserInfoService userInfoService;

    /**
     * 为登录添加用户权限
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        ......
        // 具体内容在下面分解
    }

    /**
     * 网络安全,忽略一些 ant 风格的请求,用户无法直接获取网站的资源
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        System.out.println("websecurity");
        ......
        // 具体内容在下面分解
    }

    /**
     * 这个是主要配置,对 http 请求加限定,包括请求成功和失败的以及退出的处理
     * 在 HttpSecurity 类的注解中详细介绍了各种方法调用
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        System.out.println("HttpSecurity");
        ......
        // 具体内容在下面分解
    }
}
           

一共 override 了三个方法,三个方法的调用顺序是:

添加用户权限
HttpSecurity
websecurity
           

首先

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    System.out.println("添加用户权限");

    auth.userDetailsService(userInfoService).passwordEncoder(new BCryptPasswordEncoder());
}
           

设置 AuthenticationManagerBuilder (身份认证管理器),用来查询用户名和密码的处理器设置成自己的处理器方法,然后为认证配置设置加密方式。

接着

/**
 * 网络安全,忽略一些 ant 风格的请求,用户无法直接获取网站的资源
 * @param web
 * @throws Exception
 */
@Override
public void configure(WebSecurity web) throws Exception {
    System.out.println("websecurity");
    web.ignoring().antMatchers("/index.html", "/static/**", "/login_p",
            "/favicon.ico","/swagger-ui.html","/webjars/**","/swagger-resources/**","/v2/**");
}
           

最后

/**
 * 这个是主要配置,对 http 请求加限定,包括请求成功和失败的以及退出的处理
 * 在 HttpSecurity 类的注解中详细介绍了各种方法调用
 * @param http
 * @throws Exception
 */
@Override
protected void configure(HttpSecurity http) throws Exception {
    System.out.println("HttpSecurity");

    http.
            authorizeRequests() // 定义需要保护的和不需要保护 URL
            .and()
            // 允许登录的 uri 请求
            .formLogin().loginProcessingUrl("/login")
            // 使用用户名和密码
            .usernameParameter("username").passwordParameter("password")
            // 登录失败处理
            .failureHandler(new AuthenticationFailureHandler() {
                @Override
                public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                    httpServletResponse.setContentType("application/json;charset=utf-8");// 设置编码格式
                    String result = "";

                    System.out.println();


                    // 对各种异常进行分类处理
                    if (e instanceof UsernameNotFoundException) {
                        result = "账户名不存在";
                    } else if (e instanceof LockedException) {
                        result = "登录失败";
                    } else if (e instanceof BadCredentialsException ){
                        result = "密码错误";
                    }

                    // 写入登录失败信息
                    httpServletResponse.setStatus(401);
                    ObjectMapper om = new ObjectMapper();
                    PrintWriter out = httpServletResponse.getWriter();
                    out.write(om.writeValueAsString(result));
                    out.flush();
                    out.close();
                }
            })
            // 登录成功处理
            .successHandler(new AuthenticationSuccessHandler() {
                @Override
                public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                    httpServletResponse.setContentType("application/json;charset=utf-8");

                    // 获取当前登录对象的主体,里面也包含了对象的权限
                    UserInfo userInfo = (UserInfo) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
                    HttpSession session = httpServletRequest.getSession();
                    session.setAttribute("operationUserId", userInfo.getId());
                    ObjectMapper om = new ObjectMapper();
                    PrintWriter out = httpServletResponse.getWriter();

                    // 登录成功将登录对象返回
                    out.write(om.writeValueAsString(userInfo));
                    out.flush();
                    out.close();
                }
            })
            .permitAll() // 设置所有人都可以访问登地址
            .and()
            .logout()
            .logoutUrl("/logout")
            .logoutSuccessHandler(new LogoutSuccessHandler() {
                @Override
                public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                    httpServletResponse.setContentType("application/json;charset=utf-8");
                    String result = "注销成功";
                    ObjectMapper om = new ObjectMapper();
                    PrintWriter out = httpServletResponse.getWriter();
                    out.write(om.writeValueAsString(result));
                    out.flush();
                    out.close();
                }
            })
            .permitAll()
            .and()
            .authorizeRequests()
            .antMatchers("/", "/login", "/logout").permitAll()//不拦截的地址
            .anyRequest()//剩下所有请求 进行拦截
            .authenticated();
    http.csrf().disable();//跨站请求伪造的防护,这里关掉

}
           

这里只是对登录和退出进行处理,限定登录的 uri 为 login,退出的 uri 为 layout,这两条 uri 是允许所有人进行访问的,对登录成功和登录失败进行处理,对退出进行处理。

其中对于登录成功的处理是将用户的详细信息传递给前端,里面包含了用户的权限(Role),对于不同的用户进行区分处理的时候,传递的对象可能会更加的复杂,像是包含允许此类用户进行访问的地址。

各种处理方法可以单独的写一个类进行传参处理,这里内容较小就写在了一起。

下面是完成管理员登陆的信息。

springsecurity权限验证

继续阅读