Spring Security学习记录基础版(一)
- 一个Java萌新的学习记录,文章中可能说的不严谨或者错的地方,请指出,希望能跟大家一起学习。
1.概要
- Spring Security的核心功能
- 用户认证:就是系统认为用户是否登录
- 用户授权:就是系统判断用户是否有权限去做某些事情
2.Hello Security
-
创建一个SpringBoot工程
-
引入相关依赖
<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>
-
编写controller测试
@RestController @RequestMapping("quick") public class QuickController { @GetMapping("/hello") public Object hello(){ return "hello security"; } }
-
测试结果:security启动默认会有一个登录页,security默认得用户名是:user 密码每次执行会在控制台打印,登录成功后显示测试结果
3.SpringSecurity基本原理
-
SpringSecurity本质是一个过滤器链,由很多过滤器组成
-
FilterSecurityInterceptor:是一个方法级的权限过滤器,基本位于过滤链的最底部
-
ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中的异常
-
UsernamePasswordAuthenticationFilter:对/login的POST请求做拦截,校验表单中用户名,密码
-
SpringSecurity两个重要的接口
- UserDetailsService接口:查询数据库用户名和密码的过程
- 创建类继承UsernamePasswordAuthenticationFilter,重写三个方法
- 创建类实现UserDetailService,编写查询数据过程,返回User对象,这个User对象是Security提供的对象
- PasswordEncoder接口:数据加密接口,用于返回User对象里面密码加密
- UserDetailsService接口:查询数据库用户名和密码的过程
4.security设置用户和密码
-
通过配置文件设置(了解)
spring: security: user: name: admin password: 123456 roles: root
-
通过配置类设置(了解)
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired PasswordEncoder passwordEncoder; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //将密码加密 String password = passwordEncoder.encode("123456"); auth.inMemoryAuthentication().withUser("test") .password(password).roles("test"); } //不加这一步会报@PreAuthorizeConsider defining a bean of type 'org.springframework.security.crypto.password.PasswordEncoder' in your configuration. @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
-
自定义实现类设置
-
第一步 创建配置类,设置使用哪个userDetailsService实现类
@Configuration public class MySecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
-
第二步 编写实现类,返回User对象,User对象有用户名密码和操作权限
@Service public class MyUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role"); return new User("test",new BCryptPasswordEncoder().encode("123"),auths); } }
-
5.查询数据库完成用户认证
-
引入相关依赖
<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>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.23</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
-
创建数据库
CREATE DATABASE test USE test CREATE TABLE users( id INT(10) PRIMARY KEY AUTO_INCREMENT, uname VARCHAR(30) NOT NULL, upassword VARCHAR(80) NOT NULL ) INSERT INTO users VALUES (DEFAULT,'admin','123456'); INSERT INTO users VALUES (DEFAULT,'test','123'); INSERT INTO users VALUES (DEFAULT,'root','123456')
-
配置数据库相关信息
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8 username: root password: 123456
-
创建users表对应实体类
@Data @AllArgsConstructor @NoArgsConstructor public class Users { private long id; private String uname; private String upassword; }
-
整合mybatisPlus,创建接口,继承mp的接口
public interface UserMapper extends BaseMapper<Users> { }
-
在MyUserDetailsService调用mapper里面的方法查询数据库进行用户认证
@Service public class MyUserDetailsService implements UserDetailsService { @Autowired UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //mybatisplus提供的条件构造器 QueryWrapper<Users> wrapper = new QueryWrapper(); //下面这句话等同于where uname=? wrapper.eq("uname",username); //因为用户名不能重复,使用selectOne得到某一条数据 Users user = userMapper.selectOne(wrapper); if(user == null){ throw new UsernameNotFoundException("用户不存在"); } List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role"); //查询数据库得到的user对象的用户名和密码返回给security提供的User对象里面 return new User(user.getUname(),new BCryptPasswordEncoder().encode(user.getUpassword()),auths); } }
-
在启动类上添加@MapperScan注解
@SpringBootApplication @MapperScan("com.project.securityquickstart.mapper") public class SecurityQuickStartApplication { public static void main(String[] args) { SpringApplication.run(SecurityQuickStartApplication.class, args); } }
-
启动测试
@RestController @RequestMapping("quick") public class QuickController { @GetMapping("/hello") public Object hello(){ return "hello security"; } }
测试成功,三个账号都可以进行登录,但是启动时出现如下警告: This primary key of “id” is primitive !不建议如此请使用包装类 in Class: “com.project.securityquickstart.bean.Users”
6.自定义登录页
-
配置类编写相关的配置
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() //自定义自己的登录页面 .loginPage("/login.html") //登录页面设置 .loginProcessingUrl("/quick/login") //登录页访问路径 .defaultSuccessUrl("/quick/index").permitAll() //登录成功后跳转的地址 .and().authorizeRequests() //配置不用认证就能访问的页面 .antMatchers("/","/quick/login","/quick/hello").permitAll() .anyRequest().authenticated() .and().csrf().disable(); //关闭csrf防护 }
-
创建登录的页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>security自定义登录页</title> </head> <body> <form action="/quick/login" method="post"> <input type="text" name="username"/> <input type="password" name="password"/> <input type="submit" value="Login"/> </form> </body> </html>
-
编写controller测试
@RestController @RequestMapping("quick") public class QuickController { @GetMapping("/hello") public Object hello(){ return "hello security"; } @GetMapping("/index") public Object login(){ return "hello index"; } }
-
效果图
-
当我输入http://localhost:8080/quick/index的时候会自动跳转登录页
-
登录成功后会跳转到配置类里配置的/quick/index的controller
-
7.基于权限进行访问控制
-
如果当前的主体具有指定权限,则返回true,否则返回false
-
没有访问权限 403
-
hasAuthority和hasAnyAuthority方法的使用实例
-
在配置类编写基于权限进行访问控制的相关配置
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() //自定义自己的登录页面 .loginPage("/login.html") //登录页面设置 .loginProcessingUrl("/quick/login") //登录页访问路径 .defaultSuccessUrl("/quick/index").permitAll() //登录成功后跳转的地址 .and().authorizeRequests() //配置访问权限为admins的才能访问/quick/index,只能配置单个 // .antMatchers("/quick/index").hasAuthority("admins") //配置访问权限为admins和role的都能访问/quick/index,可配置多个 .antMatchers("/quick/index").hasAnyAuthority("admins,role") //配置不用认证就能访问的页面 .antMatchers("/","/quick/login","/quick/hello").permitAll() .anyRequest().authenticated() .and().csrf().disable(); //关闭csrf防护 }
-
在MyUserDetailsService里配置用户的访问权限
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
-
8.基于角色进行访问控制
-
如果当前主题具有指定角色,则返回true
-
hasRole和hasAnyRole使用实例
-
在配置类编写基于角色进行访问控制的相关配置
//配置角色为root的才能访问/quick/index,只能配置单个角色 // .antMatchers("/quick/index").hasRole("root") //配置角色为root和admin中的一个的就能访问/quick/index,可配置多个角色 .antMatchers("quick/index").hasAnyRole("root,admin")
-
在MyUserDetailsService里配置用户的角色
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role,ROLE_root,ROLE_admin");
-
注意:security底层的hasRole和hasAnyRole方法会给你加上"ROLE_"的前缀,在配置用户角色的时候要记得加上
-
9.自定义403页面
-
在配置类中编写
http.exceptionHandling().accessDeniedPage("/unauth.html");
-
编写unauth.html页面,效果图:
10.注解的使用
10.1@Secured --用户具有某个角色,可以访问方法
-
在启动类或配置类开启注解
@EnableGlobalMethodSecurity(securedEnabled = true) public class SecurityQuickStartApplication { public static void main(String[] args) { SpringApplication.run(SecurityQuickStartApplication.class, args); } }
-
在controller添加@Secured能够访问的角色
@GetMapping("/getUpd")
@Secured("ROLE_root")
public Object upd(){
return "hello,update";
}
-
在MyUserDetailsService查看有没有配置能访问的角色并测试
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role,admins,ROLE_root");
10.2@PreAuthorize --在进入方法之前进行权限和角色验证
-
在启动类或配置类开启注解
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
-
在在controller添加@PreAuthorize能够访问的角色和权限
@GetMapping("/getUpd") @PreAuthorize("hasAuthority('role')") public Object upd(){ return "hello,update"; }
-
在MyUserDetailsService查看有没有配置能访问的角色并测试
-
这个注解里面可以写hasRole、hasAnyRole、hasAuthority和hasAnyAuthority
10.3@PostAuthorize --在方法执行之后进行权限和角色验证
-
在启动类或配置类开启注解 --同上
-
在controller添加@PreAuthorize能够访问的角色和权限
@GetMapping("/getUpd") // @Secured("ROLE_root") // @PreAuthorize("hasAuthority('role')") @PostAuthorize("hasAuthority('role')") public Object upd(){ System.out.println("update执行中"); return "hello,update"; } }
-
在MyUserDetailsService查看有没有配置能访问的角色并测试,测试发现虽然没权限访问,但是里面的方法还是打印了
- 这个注解里面可以写hasRole、hasAnyRole、hasAuthority和hasAnyAuthority
10.4@PostFilter --方法返回的数据进行过滤
-
在controller注解中添加@PostFilter 需要过滤的数据,可以搭配上面的注解一起使用
@GetMapping("/getAll") @PreAuthorize("hasAuthority('role')") @PostFilter("filterObject.uname == 'admin1'") public List<Users> getAll(){ List<Users> list = new ArrayList<Users>(); list.add(new Users(0,"admin1","123")); list.add(new Users(0,"admin2","123")); return list; }
-
测试结果,只返回指定过滤的值
10.5@PreFilter --方法传入的数据进行过滤
- 暂时还不知道怎么用,出现如下错误
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.IllegalArgumentException: Filter target must be a collection, array, map or stream type, but was Users(uname=admin1, upassword=123456)] with root cause
11.用户注销
-
在登录页添加一个退出的超链接
<a href="/logout">注销</a>
-
在配置类中添加退出映射地址
http.logout().logoutUrl("/logout").logoutSuccessUrl("/quick/exit").permitAll();
-
测试类测试
@GetMapping("/exit") public Object exit(){ return "注销成功。。。。"; }
-
测试结果注销是能注销,但是在访问其他方法不需要登录验证了
12.自动登录
-
创建保存用户token的数据库
CREATE TABLE persistent_logins( username VARCHAR(64) NOT NULL, series VARCHAR(64) NOT NULL, token VARCHAR(64) NOT NULL, last_used TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY(series) )ENGINE = INNODB DEFAULT CHARSET=utf8;
-
配置类,注入数据源,配置操作数据库对象
@Autowired private DataSource dataSource; @Bean public PersistentTokenRepository persistentTokenRepository(){ JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); return jdbcTokenRepository; }
-
配置类中配置自动登录
.and().rememberMe().tokenRepository(persistentTokenRepository()) //配置自动登录的有效时间 .tokenValiditySeconds(120) .userDetailsService(userDetailsService)
-
在登录页面添加复选框
/*name必须叫remember-me,因为security内部封装了只识别这个名字的方法*/ <input type="checkbox" name="remember-me" title="自动登录"> 自动登录
13.csrf防护
- 默认开启的,了解就行