现在是大三暑假,有机会来到企业里面学习一些东西,在接触一段 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的控制台会出现一个初始的密码:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnL5kDOzIzM1AjM5IDOwkTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
浏览器输入:http://localhost:8080 会得到一个默认的用户登录页面:
可以在启动类中将这个默认的登录验证框去掉:
@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 授权的内容。
最基本的安全配置信息在 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),对于不同的用户进行区分处理的时候,传递的对象可能会更加的复杂,像是包含允许此类用户进行访问的地址。
各种处理方法可以单独的写一个类进行传参处理,这里内容较小就写在了一起。
下面是完成管理员登陆的信息。