SpringSecurity学习
demo : https://gitee.com/houchen1996/spring-security-demo
一、SpringSecurity框架简介
SpringSecurity基于Spring框架,提供了一套Web应用安全性的完整解决方案
一般来说,Web应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是
SpringSecurity的重要核心功能
(1)用户认证:系统认为用户是否能登录。用户认证一般要求用户提供用户名和密码,系统校验用户名和密码来完成认证过程
(2)用户授权:系统判断用户是否有权限去做某些事情。在一个系统中,不同用户所具有的权限是不同的,系统会
为不同的用户分配不同的角色,而每个角色对应一系列的权限
一般来说,常见的安全管理技术栈的组合是这样的:
• SSM + Shiro
• Spring Boot/Spring Cloud + Spring Security
二、SpringSecurity入门案例
1、引入SpringSecurity依赖的现象
当引入SpringSecurity依赖后,访问接口,会默认掉转到该页面
2、SpringSecurity基本原理
两个重要的接口
1)UserDetailService
当什么都没有配置的时候,账号和密码是由SpringSecurity定义生成的。而在实际项目中,账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑
如果需要自定义认证逻辑时,只需要实现UserDetailService接口即可(从数据库中查询账号,密码和传递过来的账号密码进行校验的逻辑就是写在实现该接口的类中)
2)PasswordEncoder
数据加密接口,用于返回User对象里面密码加密
三、SpringSecurity WEB权限方案
1、设置用户名密码的三种方式
1)配置文件配置
spring: security: user: name: user password: 1234
2)配置类配置
@Configuration public class MySecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); auth.inMemoryAuthentication().withUser("lucy").password(encoder.encode("123")).roles("admin"); } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
3)自定义实现类设置 实现 UserDetailService接口
①、创建配置类,设置使用哪个UserDetailService实现类
@Configuration public class MySecurityConfig1 extends WebSecurityConfigurerAdapter { @Autowired 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("userDetailService") public class MyUserDetailService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("role"); User mary = new User("mary", new BCryptPasswordEncoder().encode("123"), authorities); return mary; } }
2、实现查询数据库来完成用户认证
整合 MyBatisPlus完成数据库操作
1)引入依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.0.5</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
2) MyUserDetailService中查询数据库,完成用户的认证过程,并返回用户
@Service("userDetailService") public class MyUserDetailService implements UserDetailsService { @Autowired UserMapper userMapper; //username: 表单中传来的用户 @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //1、mybatisplus条件构造器查询数据库 QueryWrapper<com.atguigu.securitydemo1.entity.User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); com.atguigu.securitydemo1.entity.User user = userMapper.selectOne(queryWrapper); //2、判断 if(user==null){ //说明用户不存在,认证失败 throw new UsernameNotFoundException("用户名不存在!"); } List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("role"); //3、从数据库查询得到的user对象,得到用户名和密码,返回 return new User(user.getUsername(), new BCryptPasswordEncoder().encode(user.getPassword()), authorities); } }
3、自定义登录页面
1)在配置类中添加相关配置
重写 WebSecurityConfigurerAdapter 类的 protected void configure(HttpSecurity http) 方法
@Configuration public class MySecurityConfig1 extends WebSecurityConfigurerAdapter { @Autowired UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() //自定义编写自己的登录页面 .loginPage("/login.html") //登录页面设置 .loginProcessingUrl("/user/login") //表单提交的路径 .defaultSuccessUrl("/test/index").permitAll() //登录成功后,跳转的路径 .and().authorizeRequests().antMatchers("/user/login","/test/hello").permitAll() //访问这些路径不需要登录认证 .anyRequest().authenticated() //访问除了上述的其他请求,都需要经过认证 .and().csrf().disable(); //关闭csrf防护 } }
2)编写登录页面
【注意】登录页面需要放在SpringBoot默认的静态资源文件夹下
3)测试
访问 /test/hello 可以直接访问,不需要经过认证
访问 /test/index,需要经过认证,要跳转到登录页,登录成功后,再跳转到 /test/index
4 基于权限、角色进行访问控制
1)hasAuthority方法
如果当前的主题具备某个权限,返回true,否则返回false
①、在配置类中设置当前访问地址有哪些权限
//当前登录用户只有admin权限才能访问 /test/index 接口
.antMatchers("/test/index").hasAuthority("admin")
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() //自定义编写自己的登录页面 .loginPage("/login.html") //登录页面设置 .loginProcessingUrl("/user/login") //表单提交的路径 .defaultSuccessUrl("/test/index").permitAll() //登录成功后,跳转的路径 .and().authorizeRequests() .antMatchers("/user/login","/test/hello").permitAll() //访问这些路径不需要登录认证 //当前登录用户只有admin权限才能访问 /test/index 接口 .antMatchers("/test/index").hasAuthority("admin") .anyRequest().authenticated() //访问除了上述的其他请求,都需要经过认证 .and().csrf().disable(); //关闭csrf防护 }
②、在UserDetailService中,把返回的User对象设置权限
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //1、mybatisplus条件构造器查询数据库 QueryWrapper<com.atguigu.securitydemo1.entity.User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); com.atguigu.securitydemo1.entity.User user = userMapper.selectOne(queryWrapper); //2、判断 if(user==null){ //说明用户不存在,认证失败 throw new UsernameNotFoundException("用户名不存在!"); } List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("admin"); //3、从数据库查询得到的user对象,得到用户名和密码,返回 return new User(user.getUsername(), new BCryptPasswordEncoder().encode(user.getPassword()), authorities); }
2)HasAnyAuthority()方法
如果当前的主体有任何角色的话(逗号分隔的字符串列表),返回true
.antMatchers("/test/index").hasAnyAuthority("admin,manage")
3) hasRole()方法
如果用户具备给定角色就允许访问,否则返回403
类似于hasAuthority() 如果当前主题具有指定的角色,返回true
.antMatchers("/test/index").hasRole("sale") //注意: 给用户主体设置角色时,必须要加上 ROLE_前缀,否则会访问报错 List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin");
4)hasAnyRole() 方法
如果用户具备给定角色中的人一个就允许访问,否则返回403
.antMatchers("/test/index").hasRole("sale,manage") //注意: 给用户主体设置角色时,必须要加上 ROLE_前缀,否则会访问报错 List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_manage");
5、自定义403页面
当用户主体没有权限访问某个接口时,会返回403
如何配置自定义403页面
①、static文件夹下,编写 unauth.html页面
②、配置类中添加自定义403页面的配置
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() //自定义编写自己的登录页面 .loginPage("/login.html") //登录页面设置 .loginProcessingUrl("/user/login") //表单提交的路径 .defaultSuccessUrl("/test/index").permitAll() //登录成功后,跳转的路径 .and().authorizeRequests() .antMatchers("/user/login","/test/hello").permitAll() //访问这些路径不需要登录认证 //当前登录用户只有admin权限才能访问 /test/index 接口 //.antMatchers("/test/index").hasAuthority("admin") //.antMatchers("/test/index").hasAnyAuthority("admin,manage") .antMatchers("/test/index").hasRole("sale") .anyRequest().authenticated() //访问除了上述的其他请求,都需要经过认证 .and().csrf().disable(); //关闭csrf防护 //配置没有权限访问,跳转的自定义页面 http.exceptionHandling().accessDeniedPage("/unauth.html"); }
6、认证授权 注解使用
1)@Secured :
用户具有某个角色,则可以访问该方法 。相当于配置类中添加
.antMatchers("/test/index").hasAnyRole("admin,manage")
①、启动类上开启SpringSecurity注解功能
@EnableGlobalMethodSecurity(securedEnabled = true) @MapperScan("com.atguigu.securitydemo1.mapper") @SpringBootApplication public class Securitydemo1Application { public static void main(String[] args) { SpringApplication.run(Securitydemo1Application.class, args); } }
②、controller 方法上添加 @Secured({"sale","admin"}) ,表明哪些角色可以访问该controller
@Secured({"sale","admin"}) @GetMapping("/testAnnotation") public String testAnnotation(){ return "testAnnotation"; }
2)preAuthorize:
注解适合进入方法前的权限验证,用户具备某个权限,才可以访问该方法
相当于配置类中 antMatchers("/test/index").hasAnyAuthority("admin,manage")
启动类上开启SpringSecurity注解功能
①、启动类上开启SpringSecurity注解功能
@EnableGlobalMethodSecurity(prePostEnabled = true)
controller方法上添加@PreAuthorize("hasAnyAuthority('admin')"),表明具有admin权限的主体,才能访问该接口
@GetMapping("/preAuthorize") @PreAuthorize("hasAnyAuthority('admin')") public String testPreAuthorize(){ return "preAuthorize"; }
3)postAuthrize
4) PostFilter
对方法返回的数据进行过滤
5)preFilter
对传入方法的数据进行过滤
7、用户注销
protected void configure(HttpSecurity http) throws Exception { http.formLogin() //自定义编写自己的登录页面 .loginPage("/login.html") //登录页面设置 .loginProcessingUrl("/user/login") //表单提交的路径 .defaultSuccessUrl("/success.html").permitAll() //登录成功后,跳转的路径 .and().authorizeRequests() .antMatchers("/user/login","/test/hello").permitAll() //访问这些路径不需要登录认证 //当前登录用户只有admin权限才能访问 /test/index 接口 .antMatchers("/test/index").hasAuthority("admin") .antMatchers("/test/index").hasAnyAuthority("admin,manage") .antMatchers("/test/index").hasRole("sale") .anyRequest().authenticated() //访问除了上述的其他请求,都需要经过认证 .and().csrf().disable(); //关闭csrf防护 //配置没有权限访问,跳转的自定义页面 http.exceptionHandling().accessDeniedPage("/unauth.html"); //退出 /logout=> SpringSecurity自带的请求 http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll(); }
8、SpringSecurity自动登录
整体流程:SpringSecurity会在认证成功后,将加密串同时写入浏览器的cookie和数据库,
再次访问时,会拿着cookie中的内容到数据库进行比对
SpringSecurity 对此过程做的封装(原理)
具体实现
1)、配置类,注入数据源,配置操作数据库对象
@Autowired DataSource dataSource; @Bean public PersistentTokenRepository persistentTokenRepository(){ JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); // jdbcTokenRepository.setCreateTableOnStartup(true); return jdbcTokenRepository; }
2)配置类配置自动登录
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() //自定义编写自己的登录页面 .loginPage("/login.html") //登录页面设置 .loginProcessingUrl("/user/login") //表单提交的路径 .defaultSuccessUrl("/success.html").permitAll() //登录成功后,跳转的路径 .and().authorizeRequests() .antMatchers("/user/login","/test/hello").permitAll() //访问这些路径不需要登录认证 //当前登录用户只有admin权限才能访问 /test/index 接口 .antMatchers("/test/index").hasAuthority("admin") .antMatchers("/test/index").hasAnyAuthority("admin,manage") .antMatchers("/test/index").hasRole("sale") .anyRequest().authenticated() //访问除了上述的其他请求,都需要经过认证 .and().csrf().disable(); //关闭csrf防护 //配置没有权限访问,跳转的自定义页面 http.exceptionHandling().accessDeniedPage("/unauth.html"); http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll() //退出 // 配置自动登录 .and().rememberMe().tokenRepository(persistentTokenRepository()) .tokenValiditySeconds(60) .userDetailsService(userDetailsService); }
3)登录时,会携带cookie,并且数据库中生成一条记录,然后关闭浏览器后再访问 /test/index , 发现直接可以访问
不需要重新登录
9、csrf
跨站请求伪造(Cross-site request forgery)
原因: 简单的身份验证只能保证请求发自某个用户的浏览器,确并不能保证请求本身是用户自愿发生的