章节
第一章链接: SpringBoot集成security(1)|(security入门)
第二章链接: SpringBoot集成security(2)|(security自定义配置)
第三章链接: SpringBoot集成security(3)|(security的前后端分离登录以及响应处理)
第四章链接: SpringBoot集成security(4)|(security基于JWT实现前后端分离自定义登录)
上一章节我们简单的介绍了,springSecurity的集成,并实现了简单的登录功能,以及security登录的一些原理,本章我们将介绍security的基于数据库用户的配置,权限控制以及资源访问控制的配置
本片文章是在上一篇基础上进行的扩展,如果大家对项目基础不是很清楚请查看上一篇文章
Spring Security是一个提供身份验证,授权和保护以防止常见攻击的框架。凭借对命令式和响应式应用程序的一流支持,它是用于保护基于Spring的应用程序的事实上的标准。Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理
要实现自定义配置,首先创建一个继承于WebSecurityConfigurerAdapter的配置类,在代码实现之前我们来了解一下WebSecurityConfigurerAdapter配置类
WebSecurityConfigurerAdapter是一个抽象类,它实现了默认的认证授权,并且提供了三个可重写的方法供开发者自定义实现认证、授权功能。
// 自定义身份认证的逻辑
protected void configure(AuthenticationManagerBuilder auth) throws Exception { }
// 自定义全局安全过滤的逻辑
public void configure(WebSecurity web) throws Exception { }
// 自定义URL访问权限的逻辑
protected void configure(HttpSecurity http) throws Exception { }
思考:上一节我们讲了security的简单应用,能够配置账户,控制请求访问,其中有几个问题1、用户不能动态调整2、用户密码是明文容易泄露3、密码比对方式简单,基于以上问题我们来实现security的用户自定义管理。
内存用户存储模式,配置如下
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 常用的三种存储方式,项目找那个用的最多的为,自定义用户存储
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//1、内存用户配置
auth.inMemoryAuthentication().passwordEncoder(bCryptPasswordEncoder())
.withUser("admin").password(bCryptPasswordEncoder().encode("123456")).authorities("ADMIN")
.and()
.withUser("test").password(bCryptPasswordEncoder().encode("123456")).authorities("TEST");
/**
* 使用security 提供的加密规则(还有其他加密方式)
* @return
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
注:@EnableWebSecurity注解,这个注解是Spring Security用于启用web安全的注解
重写 configure(AuthenticationManagerBuilder auth)类使用inMemoryAuthentication方法(表示内存配置)配置用户,用户密码都使用了BCryptPasswordEncoder的加密方式,
注:如果密码不使用加密方式会报错:Encoded password does not look like BCrypt
思考:上面我们讲了基于内存的方式配置用户,用户配置在服务启动的时候会加载到内存中。这种方式的存在以下缺点,服务启动后不能变更,且用户不能动态维护。因此不可取。下面我们来介绍通过数据库方式实现用户配置
本例种采用了数据库JPA的方式实现,首先需要引入数据库相关依赖
<!--数据库引入引入-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
实体类有两个一个是数据库表对应的实体User,另外一个Security对应的用户实体JwtUser,前者用户和数据库交互数据,后者用于和security交互数据。
@Data
@Entity
@Table(name = "t_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
@Column(name = "username")
private String username;
@Column(name = "password")
private String password;
@Column(name = "role")
private String role;
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", role='" + role + '\'' +
'}';
}
}
@Data
public class JwtUser implements UserDetails {
private Integer id;
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
public JwtUser(User user) {
id = user.getId();
username = user.getUsername();
password = user.getPassword();
authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole()));
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public String toString() {
return "JwtUser{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", authorities=" + authorities +
'}';
}
}
注意:JwtUser 实体类中有几个默认的方法isAccountNonExpired(),isAccountNonExpired(), isCredentialsNonExpired(), isEnabled()这几个方法返回类型为true,主要用于security的一些初始化工作,当设置的值被改后运行登录就会出现异常。
本例种采用了JPA的方式实现,代码简单省事,不需要编写接口的实现
public interface UserRepository extends CrudRepository<User, Integer> {
/**
* 根据用户名查询用户
* @param username
* @return
*/
User findByUsername(String username);
}
注册用户,用来在数据库中添加登录用户
import com.oak.net.response.ResponseHandle;
import com.oak.net.rest.entity.dto.User;
import com.oak.net.rest.service.jpa.UserRepository;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Api(tags = {"演示相关接口"})
@RestController
@RequestMapping("/demo")
public class DemoCtrl {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@ApiOperation(value = "获取接口", notes = "获取接口")
@GetMapping(value = "/get")
public ResponseHandle success() {
List list = new ArrayList<>();
return ResponseHandle.SUCCESS("获取数据成功");
}
@ApiOperation(value = "注册用户", notes = "注册")
@PostMapping("/register")
public String registerUser(@RequestBody Map<String, String> registerUser) {
User user = new User();
user.setUsername(registerUser.get("username"));
user.setPassword(bCryptPasswordEncoder.encode(registerUser.get("password")));
user.setRole("ROLE_USER");
User save = userRepository.save(user);
return save.toString();
}
}
UserDetailsService为securit提供接口,该接口提供了一个loadUserByUsername()方法,方法用于实现从数据库中根据用户名查询用户信息并传递给spring security,传递的形式就是使用上文说的UserDetails实体。
下图梳理一下UserDetailsService在securit架构中的作用
Spring Security中进行身份验证的是AuthenticationManager接口,ProviderManager是它的一个默认实现,但它并不用来处理身份认证,而是委托给配置好的AuthenticationProvider,每个AuthenticationProvider会轮流检查身份认证。检查后或者返回Authentication对象或者抛出异常。UserDetailsService用在AuthenticationProvider的实现类中查询用户信息。
UserDetailsService代码实现
@Service
public class UserServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
/**
*
* @param s
* @return 实现loadUserByUsername方法,根据用户名查找用户信息
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userRepository.findByUsername(s);
return new JwtUser(user);
}
}
WebSecurityConfigurerAdapter自定义实现类中需要重写configure(AuthenticationManagerBuilder auth)方法,和上面的内存配置一箱,只是配置方法不同。配置如下:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceImpl userService;
/**
* 常用的三种存储方式,项目找那个用的最多的为,自定义用户存储
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//3、自定义用户存储
auth.userDetailsService(userService)
.passwordEncoder(bCryptPasswordEncoder());
}
/**
* 使用security 提供的加密规则(还有其他加密方式)
*
* @return
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
在浏览器访问接口http://localhost:8080/demo/get,会跳转到http://localhost:8080/login页面,在登录页面输入user用户(自己初始化到数据库中的用户),会提示访问成功。
本步骤实现了在数据库中添加用户,并能够实现登录认证。
思考:上面我们实现了请求访问控制需要认证,但是有些资源并不重要,我们不想所有的都需要认证后才能访问,例如静态资源、swagger资源等。接下来我们会完成怎么去掉这些静态资源的访问控制。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 前面小结配置此处省略
/**
* configure(WebSecurity)用于影响全局安全性(配置资源,设置调试模式,通过实现自定义防火墙定义拒绝请求)的配置设置。
* 一般用于配置全局的某些通用事物,例如静态资源等
* @param web
*/
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS, "/**") ///跨域请求预处理
.antMatchers("/favicon.ico")
.antMatchers("/swagger**") // 以下swagger静态资源、接口不拦截
.antMatchers("/doc.html")
.antMatchers("/swagger-resources/**")
.antMatchers("/v2/api-docs")
.antMatchers("/webjars/**")
.antMatchers("/test/**");
}
}
思考:上面我们实现了静态资源以及共用资源的访问控制放开,但是有一些接口我们也希望能够做一些定制配置,例如删除接口我们希望ADMIN用户才能访问,查询接口不登录也能访问。下面我们来实现接口的相关配置。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 前面小结配置此处省略
/**
* 配置接口拦截
* configure(HttpSecurity)允许基于选择匹配在资源级配置基于网络的安全性,
* 也就是对角色所能访问的接口做出限制
* @param httpSecurity 请求属性
* @throws Exception
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.antMatchers(HttpMethod.GET, "/demo/get").permitAll()
//指定权限为ROLE_ADMIN才能访问,这里和方法注解配置效果一样,但是会覆盖注解
.antMatchers("/demo/delete").hasRole("ADMIN")
// 所有请求都需要验证
.anyRequest().authenticated()
.and()
//.httpBasic() Basic认证,和表单认证只能选一个
// 使用表单认证页面
.formLogin()
//配置登录入口,默认为security自带的页面/login
.loginProcessingUrl("/login")
.and()
// post请求要关闭csrf验证,不然访问报错;实际开发中开启,需要前端配合传递其他参数
.csrf().disable();
}
我们访问localhost:8080/demo/delete接口,页面会跳转到localhost:8080/login,当我们用配置的普通用户登陆时页面会出现403禁止访问的信息,这是应为我们给该请求配置了.hasRole(“ADMIN”),只有登陆用户有ADMIN角色才能访问,换成admin用户登陆,会提示成功。
思考:上面的请求异常很暴力,摆出一堆状态码,非开发人员很难明白到底发生了什么,我们可以自定义一个友好的提示信息,这样用户就可以很直白的感受到哪里出错。下一章节我们将实现用户的自定义登录,以及登录异常的自定义处理。
到此springboot集成了security完成了用户的数据库配置登录,以及接口资源的访问权限控制。如果项目为前后端一体的,现在基本能满足一个小型项目的认证授权功能了。
第一章链接: SpringBoot集成security(1)|(security入门)
第二章链接: SpringBoot集成security(2)|(security自定义配置)
第三章链接: SpringBoot集成security(3)|(security的前后端分离登录以及响应处理)
第四章链接: SpringBoot集成security(4)|(security基于JWT实现前后端分离自定义登录)