文章目录
一、Hello Spring Security
1.1. 创建一个Spring Security 项目
使用IDEA
的Spring Initializr 向导创建SpringBoot项目,导入Web和Spring Security依赖
打开pom.xml,核心就是引入以下的依赖:
<dependencies>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
新建一个controller方便后续的测试
package cn.quguai.controller;
@RestController
public class HelloController {
@GetMapping("/")
public String hello(){
return "Hello Spring Security";
}
}
直接运行项目即可。并访问http://localhost:8080
,可以看到下面的页面。用户名为user
,密码来源于项目的控制台,是项目随机生成的。
:浏览器显示的内容
除了以上的方式,还可以通过修改配置文件中的内容来自定义用户名和密码
spring.security.user.name=admin
spring.security.user.password=12345
spring.security.user.roles=admin
注意:以上的方式仅是针对于刚入门的教程,绝大多数web应用并不会采用这种HTTP基本认证服务,除了安全性差。无法携带cookie的基本信息外,灵活性差也是一个主要的缺点。通常来讲大家更愿意选择表单验证,自己实现表单验证和,从而提高安全性。
二、表单验证
2.1 默认表单验证
使用Spring Initializr 初始化一个SpringBoot项目,并新建一个配置类。
自定义的配置类需要去继承WebSecurityConfigurerAdapter
并增加@EnableWebSecurity
注解,其中该注解点开以后会发现已经有了@Configuration
注解,相当于已经加入到容器当中。
@EnableWebSecurity // 包含了@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
2.2 自定义表单登录页
- 新建一个controller来处理我们登录成功以后的请求
@RestController
public class MyController {
@GetMapping("/myLogin")
public String myLogin(){
return "Login Success!!";
}
}
- 初步配置自定义表单登陆页
此时我们就需要重写configure
方法
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html") // 自定义登录页
.loginProcessingUrl("/login") // form表单登录action
.defaultSuccessUrl("/myLogin").permitAll() // 登陆成功后的请求
.and().authorizeRequests()
.antMatchers("/css/**", "/img/**", "/js/**").permitAll() // 不对这些请求认证
.anyRequest().authenticated() // 其他所有请求都需要认证
.and()
.csrf().disable(); // 关闭crsf拦截请求
}
}
- 编写自定义登陆页面
<form action="/login" method="post">
<input name="username" type="text" placeholder="手机号/邮箱">
<input name="password" type="password" placeholder="密码">
<input type="submit" value="登录">
</form>
以上这是列出核心的html,还可以自己进行丰富,主要前两个input框的name必须是
username
和passowrd
。以上的页面放入到resources/static目录下。
三、认证与授权
3.1 默认数据库模型的认证与授权
1. 资源准备
在controller下新建三个Controller,分别代表三种场景,admin(管理员)、User(普通用户)、App(开放请求)。
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("api")
public String hello(){
return "admin Hello";
}
}
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("api")
public String hello(){
return "user Hello";
}
}
@RestController
@RequestMapping("/app")
public class AppController {
@GetMapping("api")
public String hello(){
return "app Hello";
}
}
2. 资源授权相关配置
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.and().authorizeRequests()
.antMatchers("/app/api/**").permitAll()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/api/**").hasRole("USER")
.anyRequest().authenticated();
}
}
antMatchers()
是一个采用ANT模式的URL匹配器。ANT模式使用?
可以匹配任意单个字符,使用*
来匹配0个或多个字符。此时我们我们没有进行授权的相关用户操作,下面开始进行测试,启动服务。
访问 /app/api 请求无需进行登录认证
当访问其他两个两个请求都需要进行登录,按照控制台的密码进行登陆后会出现403的访问被禁止的相应。
3. 基于内存的多用户支持
目前为止都是只支持一个单用户登录,我们可以引入自定义UserDetailsService。
@Configuration
public class MyConfig {
@Autowired
private PasswordEncoder passwordEncoder;
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("user").password(passwordEncoder.encode("123")).roles("USER").build());
manager.createUser(User.withUsername("admin").password(passwordEncoder.encode("123")).roles("ADMIN", "USER").build());
return manager;
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
在这里向容器当中注册一个UserDetailsService,在这里使用InMemoryUserDetailsManager,InMemoryUserDetailsManager是UserDetailsService的一个实现类,方便在内存中创建一个用户。
这里必须创建一个PasswordEncoder,否则在登陆的时候会报出
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
异常。
注意在角色名称必须和前面的一一对应,且区分大小写。
此时进行登录会发现全部按照我们预想的那样输出结果。
访问 /user/api 使用admin和user进行登录均可以进行访问,/admin/api只能登录admin才可以进行访问,否则出现403错误。
3.2 自定义数据库模型进行认证与授权
这里我们引入SpringDataJPA进行数据库的操作。
1. 资源准备
相关pom文件增加mysql的连接和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>
在application.yml中配置数据库和jpa相关配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: LY0115..
jpa:
hibernate:
ddl-auto: update
show-sql: true
open-in-view: false
properties:
hibernate:
enable_lazy_load_no_trans: true
2. 创建UserEntity
实体类需要继承UserDetails,后续SpringSecurity会通过UserDetails进行后续的认证。主要是通过UserDetailsService的loadUserByUsername的方法获取一个UserDetails对象,该对象包含了用户的基本信息。
package cn.quguai.entity;
@Data
@Entity
@Table(name = "t_user")
public class UserEntity implements UserDetails {
@Id
@GeneratedValue
private Long id;
private String username;
private String password;
private Boolean enable;
private String roles;
@Transient
private List<? extends GrantedAuthority> authorityList;
public void setAuthorityList(List<GrantedAuthority> authorityList) {
this.authorityList = authorityList;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorityList;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.enable;
}
}
实现UserDetails定义的几个方法:
- isAccountNonLocked、isCredentialsNonExpired、isAccountNonExpired暂且用不到,全部返回true,否则SpringSecurity会认为账号存在异常;
- isEnabled对应enable字段,将其带入即可;
- getAuthorities对应roles字段,由于结构不一致,这里新建一个,后续进行填充。
3. 创建UserRepository
后续需要操作数据库通过username获取UserEntity。
@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
UserEntity findByUsername(String username);
}
4. 创建并实现UserDetailsService
需要删除事先创建的基于内存的UserDetailService
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userEntity = userRepository.findByUsername(username);
if (userEntity == null) {
throw new UsernameNotFoundException("用户不存在");
}
userEntity.setPassword(passwordEncoder.encode(userEntity.getPassword()));
userEntity.setAuthorityList(AuthorityUtils.commaSeparatedStringToAuthorityList(userEntity.getRoles()));
return userEntity;
}
}
由于前端密码都要经过SpringSecurity,为了能和数据库进行比较一致,所以需要将数据库中的密码也经过相同的算法进行加密。
由于数据库中存储的都是String的字符串,并以逗号进行分割,但是实体类中存储的却是
GrantedAuthority
,需要通过AuthorityUtils
下的commaSeparatedStringToAuthorityList
** 进行转换。**
5. 启动项目
在项目启动后,会自动创建数据库表,在数据库表中创建两个用户。方便后续的测试。
3.3 基于注解实现权限配置
1. 资源准备
修改WebSecurityConfig类,将原来的权限配置进行删除。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.and().authorizeRequests()
.antMatchers("/app/api").permitAll()
//.antMatchers("/admin/api/**").hasRole("ADMIN")
//.antMatchers("/user/api/**").hasRole("USER")
.anyRequest().authenticated();
}
}
2. 开启全局配置
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@SpringBootApplication
public class AuthenticationApplication {
public static void main(String[] args) {
SpringApplication.run(AuthenticationApplication.class, args);
}
}
3. 使用注解
- @Secured 可以传入一个String数组,代表拥有这些权限的都可以进行访问(如果是角色必须带上"ROLE_"的前缀)
- @PreAuthorize可以传入一个函数进行判断是否包含这个角色:在方法执行之前进行判断
- @PostAuthorize 和 @PreAuthorize类似只不过是在方法执行以后继续判断
@RestController
@RequestMapping("/user")
public class UserController {
@PreAuthorize("hasRole('USER')") //在方法之前进行验证
@PostAuthorize("hasAuthority('USER')") //在方法以后进行验证
@Secured({"ROLE_USER"})
@GetMapping("api")
public String hello(){
return "user Hello";
}
}