一、SpringSecurity 基本介绍 1.1、概要 Spring是非常流行和成功的Java应用开发框架,Spring Security正是Spring家族中的成员。Spring Security基于Spring框架,提供了一套Web应用安全性的完整解决方案。
正如你可能知道的关于安全方面的两个主要区域是“认证”和“授权”(或者访问控 制),一般来说,Web应用的安全性包括用户认证(Authentication)和用户授权 (Authorization)两个部分,这两点也是Spring Security重要核心功能。
1、用户认证 验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。通俗点说就是系统认为用户是否能登录
2、用户授权 验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情。
1.2、对比 1、SpringSecurity
和Spring无缝整合。
全面的权限控制。
专门为Web开发而设计。
旧版本不能脱离Web环境使用。 新版本对整个框架进行了分层抽取,分成了核心模块和Web模块。单独引入核心模块就可以脱离Web环境。 重量级
2、Shiro Apache旗下的轻量级权限控制框架。
Shiro主张的理念是把复杂的事情变简单。针对对性能有更高要求 的互联网应用有更好表现。
通用性。好处:不局限于Web环境,可以脱离Web环境使用。 缺陷:在Web环境下一些特定的需求需要手动编写代码定制。 3、补充 Spring Security是Spring家族中的一个安全管理框架,实际上,在Spring Boot出现之前,Spring Security就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是Shiro的天下。
相对于Shiro,在SSM中整合Spring Security都是比较麻烦的操作,所以,SpringSecurity虽然功能比Shiro强大,但是使用反而没有Shiro多(Shiro虽然功能没有Spring Security多,但是对于大部分项目而言,Shiro也够用了)。
自从有了Spring Boot之后,Spring Boot对于Spring Security提供了自动化配置方案,可以使用更少的配置来使用Spring Security。
4、技术组合 因此,一般来说,常见的安全管理技术栈的组合是这样的:
SSM + Shiro Spring Boot/Spring Cloud + Spring Security 1.3、Hello World 1、步骤 2、依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <version > 2.2.1.RELEASE</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <version > 2.2.1.RELEASE</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > <version > 2.2.1.RELEASE</version > </dependency > </dependencies >
3、控制器类 1 2 3 4 5 6 7 8 9 @RestController @RequestMapping("/test") public class TestController { @GetMapping("hello") public String hello () { return "hello" ; } }
4、启动,测试 由于在配置文件中,我指定端口为8111,所以访问 localhost:8111/test/hello
可以看到需要认证,Spring Security 默认的登录账号为user,且在控制台中生成一个随机的密码。
使用账号密码登录
二、SpringSecurity
基本原理 SpringSecurity
采用的是责任链的设计模式,它有一条很长的过滤器链, 本质上是一个过滤器链 ,每个 Filter 都有其各自的功能,而且各个 Filter 之间还有关联关系,所以它们的组合顺序也是非常重要的。
重点看三个过滤器
2.1、三个重要的过滤器 FilterSecurityInterceptor
是一个方法级的权限过滤器,基本位于过滤链的最底部。这个类实现了 Filter 接口
ExceptionTranslationFilter
是个异常过滤器,用来处理在认证授权过程中抛出的异常
UsernamePasswordAuthenticationFilter
对/login的POST请求做拦截,校验表单中用户名,密码。
2.2、UserDetailsService
接口讲解 当什么也没有配置的时候,账号和密码是由Spring Security定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。
如果需要自定义逻辑时,只需要实现UserDetailsService
接口即可。
从数据库中查询对象的逻辑应该写在此接口的实现类中
接口定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public interface UserDetailsService { UserDetails loadUserByUsername (String username) throws UsernameNotFoundException ; }
1、自定义用户认证逻辑的步骤 创建一个类,继承 UsernamePasswordAuthenticationFilter
,然后重写三个方法 重写 attemptAuthentication
方法 重写 successfulAuthentication
方法,如果认证成功,执行此方法 重写 unsuccessfulAuthentication
方法,如果认证失败,执行此方法 创建类实现 UserDetailService
接口,编写查询数据库的过程,返回一个User对象,这个 User对象是SpringSecurity
框架提供的。 2、UserDetails
接口 这个接口是系统默认的用户主体
1 2 3 4 5 6 7 8 9 10 11 12 13 Collection<?extendsGrantedAuthority> getAuthorities(); String getPassword () ;String getUsername () ;booleanisAccountNonExpired(); booleanisAccountNonLocked(); booleanisCredentialsNonExpired(); booleanisEnabled();
可以看到,UserDetails
有一个实现类–User,我们以后可以使用此实现类。
3、方法参数 username 表示用户名。此值是客户端表单传递过来的数据。默认情况下必须叫username,否则无法接收。
2.3、PasswordEncoder
接口讲解 1、接口定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public interface PasswordEncoder { String encode (CharSequence rawPassword) ; boolean matches (CharSequence rawPassword, String encodedPassword) ; default boolean upgradeEncoding (String encodedPassword) { return false ; } }
2、接口实现类
BCryptPasswordEncoder
是Spring Security官方推荐的密码解析器,平时多使用这个解析器。BCryptPasswordEncoder
是对bcrypt
强散列方法的具体实现。是基于Hash算法实现的单向加密。可以通过strength控制加密强度,默认10.
3、BCryptPasswordEncoder
常用方法演示 1 2 3 4 5 6 7 8 9 10 11 12 @Test public void test01 () { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String password = encoder.encode("wuhu" ); System.out.println("加密后的数据为:" + password); boolean result = encoder.matches("wuhu" ,password); System.out.println("结果为:" + result); }
查看结果
三、SpringSecurity
Web权限方案 3.1、设置登录用户名和密码 下面演示一下设置登录的用户名和密码的三种方式
1、通过配置文件配置 1 2 3 4 5 spring: security: user: name: wuhu password: qifei
使用我们自定义的账号密码进行登录
结果如下
2、通过配置类进行配置 编写一个配置类,这个类继承 WebSecurityConfigurerAdapter
,重写 configure 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration public class UserInfoConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String password = encoder.encode("123" ); auth.inMemoryAuthentication().withUser("wuhu" ).password(password).roles("admin" ); } @Bean public PasswordEncoder encoder () { return new BCryptPasswordEncoder(); } }
在上面的配置类中,由于 BCryptPasswordEncoder
对象默认没有,所以我们需要手动new一个,并将其放入容器中。
启动项目,进行测试,这里我们需要手动注销配置文件中的配置
查看结果
3、自定义UserDetailsService
实现类 当什么也没有配置的时候,账号和密码是由Spring Security定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。
创建配置类,设置使用哪个 UserDetailsService
实现类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(encoder()); } @Bean public PasswordEncoder encoder () { return new BCryptPasswordEncoder(); } }
编写实现类,返回User对象,User对象中有用户名、密码和操作权限 这里由于 @Autowired
默认根据类型注入,所以可以不指定名称。
1 2 3 4 5 6 7 8 @Service("userDetailsService") public class MyUserDetailsServiceImpl implements UserDetailsService { public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role" ); return new User(username,new BCryptPasswordEncoder().encode("123" ),auths); } }
查看结果
3.2、通过查询数据库完成用户认证 1、数据库脚本 密码:atguigu
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for users -- ---------------------------- DROP TABLE IF EXISTS `users`; CREATE TABLE `users` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `password` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `username`(`username`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- 密码:qifei -- ---------------------------- INSERT INTO `users` VALUES (1, 'wuhu', '$2a$10$nBH8a3Aenk4JtxC2tFwWXusmnzEytzUWME6CWyi/Vr50weaNM0Bxi'); SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for role -- ---------------------------- DROP TABLE IF EXISTS `role`; CREATE TABLE `role` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;
2、引入依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.19</version > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.3.1</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.16</version > </dependency >
3、创建实体类 1 2 3 4 5 6 @Data public class Users { private Long id; private String username; private String password; }
4、在我们自定义的 MyUserDetailsServiceImpl
中注入 UsersMapper
1 2 3 4 @Mapper @Repository public interface UsersMapper extends BaseMapper <Users > {}
5、在 MyUserDetailsServiceImpl
的 loadUserByUsername
方法中查询数据库 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Service("userDetailsService") public class MyUserDetailsServiceImpl implements UserDetailsService { @Autowired private UsersMapper usersMapper; @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { QueryWrapper<Users> wrapper = new QueryWrapper<>(); wrapper.eq("username" ,username); Users target = usersMapper.selectOne(wrapper); if (target == null ) { throw new UsernameNotFoundException("用户名不存在!" ); } System.out.println(target); List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role" ); return new User(target.getUsername(), target.getPassword(), auths); } }
6、测试
查看控制台,可以看到查询了数据库中的记录
3.3、设置自定义登录页面 这个方法和上面指定UserDetailsService
实现类和密码编码器
的 configure
不同,这里传入的参数是一个 HttpSecurity
对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Override protected void configure (HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html" ) .loginProcessingUrl("/user/login" ) .defaultSuccessUrl("/test/index" ).permitAll() .and() .authorizeRequests() .antMatchers("/" ,"/test/hello" ,"/user/login" ).permitAll() .anyRequest().authenticated() .and().csrf().disable(); }
3、编写自定义登录页面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <form action ="/user/login" method ="post" > 用户名:<input type ="text" name ="username" /> <br /> 密码:<input type ="text" name ="password" /> <br /> <input type ="submit" value ="login" > </form > </body > </html >
注意:
表单中用户名和密码输入框的 name 属性必须为 username 和 password,这是由于SpringSecurity
中设置了这两个参数的参数名
4、测试配置类中配置的信息 此时无需认证,直接就可以访问
可以看到直接跳转到我们自定义的登录页面
认证成功后自动跳转至 /test/index
3.4、基于角色或权限进行访问控制 1、hasAuthority
(基于权限) 如果当前主体拥有指定的权限,则返回true,否则返回false
在配置类中设置访问路径和对应权限,表示只有拥有某某权限的用户才能访问该路径
在我们自定义的 MyUserDetailsServiceImpl
中,为返回的User对象设置权限 这里先演示没有权限的情况
1 2 3 4 @GetMapping("admin/index") public String adminIndex () { return "只有拥有管理员权限的用户才能访问该地址...adminIndex" ; }
当前用户拥有的权限是role
修改上面代码,将角色权限为admin,重新启动程序后访问
2、hasAnyAuthority
方法(基于权限) 如果当前的用户有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回true
假设系统中一个资源,admin可以访问,manager也可以访问。
上面hasAnyAuthority
中的参数也可以写成:hasAnyAuthority("admin,manager")
1 2 3 4 @GetMapping("find") public String find () { return "拥有admin权限或者manager权限都可以访问此资源...test/find!!!" ; }
用户拥有admin角色的情况
用户拥有manager的情况
用户拥有role的情况
3、hasRole
(基于角色) 如果用户具备给定角色就允许访问,否则403
如果当前主体拥有指定的角色,则返回true
1 2 3 4 5 6 7 8 9 private static String hasRole (String role) { Assert.notNull(role, "role cannot be null" ); if (role.startsWith("ROLE_" )) { throw new IllegalArgumentException( "role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'" ); } return "hasRole('ROLE_" + role + "')" ; }
如果我们传入的是 sale
, 那么最终我们的角色是 ROLE_sale
,角色名不允许以 ROLE_
开始
修改配置类,设置某一资源只有 ROLE_sale
才能访问
在设置某资源的访问角色时可以必须不加 ROLE_,但在为用户设置角色时必须加上 ROLE_
1 List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale" );
1 2 3 4 @GetMapping("order/find") public String orderFind () { return "order模块下的find资源,只有ROLE_sale角色才能访问..." ; }
当用户角色为: ROLE_sale
时
当用户角色不为:ROLE_sale
时
访问
4、hasAnyRole
(基于角色) 表示用户具备任何一个条件,拥有要求的角色之一即可访问。
1 2 3 4 @GetMapping("order/update") public String orderUpdate () { return "order模块下的update资源,{ROLE_sale,ROLE_manager,ROLR_coder}角色才能访问..." ; }
当用户拥有ROLE_sale
角色时
当用户只拥有 ROLE_admin
时,由于给用户分配的角色不在上述的角色集合中,所以访问出错403
5、SpringSecurity
中Authority和Role的区别 hasRole
这里会自动给传入的字符串加上 ROLE_
前缀,所以在数据库中的权限字符串需要加上 ROLE_
前缀。hasAuthority
方法时,如果数据是从数据库中查询出来的,这里的权限和数据库中保存一致即可,可以不加 ROLE_
前缀。即数据库中存储的用户角色如果是 admin,这里就是 admin。如果我们设置一个资源只允许给 ROLE_admin
访问,那么我们需要在hasRole/hasAnyRole
方法中传入角色去除ROLE_
前缀的角色名。
3.5、自定义403页面 1、自定义403页面 1 2 3 4 5 6 7 8 9 10 11 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <h1 > 没有访问权限!!!</h1 > <h1 > 403</h1 > </body > </html >
2、在配置类中配置403页面 和自定义登录页面一样,同样是在configure(HttpSecurity http)
方法中定义
1 2 3 4 5 6 @Override protected void configure (HttpSecurity http) throws Exception { http.exceptionHandling().accessDeniedPage("/unauth.html" ); ...其余配置 }
3、测试 当没有访问权限时
3.6、注解使用 1、@Secured
判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀 ROLE_
,功能类似于hasAnyRoles
在使用注解之前需要在主启动类或配置类上添加 @EnableGlobalMethodSecurity(securedEnabled=true)
注解开启注解功能。
1 2 3 4 5 6 7 @EnableGlobalMethodSecurity(securedEnabled = true) @SpringBootApplication(scanBasePackages = "com.hzx") public class HelloWorldApplication { public static void main (String[] args) { SpringApplication.run(HelloWorldApplication.class); } }
1 2 3 4 5 @GetMapping("order/delete") @Secured({"ROLE_sale","ROLE_manager","ROLR_coder"}) public String orderDelete () { return "order模块下的delete资源,{ROLE_sale,ROLE_manager,ROLR_coder}角色才能访问..." ; }
测试,当前用户角色为:ROLE_admin
,所以没有权限访问
将用户的角色修改为 ROLE_sale
,再次测试,查看结果
再次测试
2、@PreAuthorize
@PreAuthorize
适合进入方法前的权限校验,@PreAuthorize
可以将登录用户的 roles/permissions
参数传入到方法中。
可以配合我们上面讲的hasRole/hasAnyRoles
设置访问该资源所需要的角色
使用hasAuthority/hasAnyAuthority
设置访问该资源所需要的权限
该注解使用前同样需要开启注解功能,在配置类或者启动类上添加 @EnableGlobalMethodSecurity(prePostEnabled =true)
注解
1 2 3 4 5 @Configuration @EnableGlobalMethodSecurity(prePostEnabled =true,securedEnabled = true) public class SecurityConfiguration extends WebSecurityConfigurerAdapter { ... }
1 2 3 4 5 @GetMapping("preAuthorize") public String preAuthorize () { System.out.println("preAuthorize" ); return "preAuthorize" ; }
在方法上添加 @PreAuthorize
注解,该注解可以设置访问此资源需要的权限或角色 设置访问权限 1 2 3 4 5 6 @GetMapping("preAuthorize") @PreAuthorize("hasAnyAuthority('menu:system','order:system')") public String preAuthorize () { System.out.println("preAuthorize" ); return "preAuthorize" ; }
此时用户没有访问权限,进行测试,访问 localhost:8111/test/preAuthorize
为用户添加权限
1 List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale,menu:system" );
重启项目,访问资源,此时提示访问成功
设置访问资源所需要的角色 1 2 3 4 5 6 @GetMapping("preAuthorize") @PreAuthorize("hasAnyRole('ROLE_管理员,ROLE_sale')") public String preAuthorize () { System.out.println("preAuthorize" ); return "preAuthorize" ; }
用户现拥有的角色为:ROLE_sale
,所以可以访问该资源
重启项目,访问
修改用户角色
1 List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_coder" );
重启项目,访问资源,此时无法访问
如果要求访问某一资源的用户需要同时具有两个角色,那么可以这样使用 1 @PreAuthorize("hasRole('normal') AND hasRole('admin')")
这样,访问此资源的用户必须同时具有 ROLE_normal
角色和 ROLE_admin
角色。
3、@PostAuthorize
@PostAuthorize
注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限.
同样需要在配置类或者启动类上先开启注解功能:@EnableGlobalMethodSecurity(prePostEnabled =true)
当没有权限的用户访问资源时,可以利用这个来留下他的访问记录,以此做出针对性的拉黑
1 2 3 4 5 6 @GetMapping("postAuthorize") @PostAuthorize("hasAnyRole('ROLE_管理员,ROLE_sale')") public String postAuthorize () { System.out.println("postAuthorize" ); return "postAuthorize" ; }
设置用户角色为 ROLE_coer
,此时用户没有权限访问 1 List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_coder" );
虽然提示没有访问权限,但控制台上输出了 postAuthorize
,证明方法被执行了
4、@PostFilter
@PostFilter
:权限验证之后对数据进行过滤
留下用户名是admin1的数据表达式中的filterObject
引用的是方法返回值List中的某一个元素
1 2 3 4 5 6 7 8 9 10 @RequestMapping("getAll") @PreAuthorize("hasRole('ROLE_管理员')") @PostFilter("filterObject.username == 'admin1'") @ResponseBody public List<Users> getAllUser () { ArrayList<Users> list =new ArrayList<>(); list.add(new Users(1L ,"admin1" ,"6666" )); list.add(new Users(2L ,"admin2" ,"888" )); return list; }
同时讲用户角色设置为 ROLE_管理员
,测试查看结果 可以看到输出结果只有 admin1
,证明过滤生效
5、@PreFilter
进入控制器之前对数据进行过滤
1 2 3 4 5 6 7 8 9 @RequestMapping("getTestPreFilter") @PreAuthorize("hasRole('ROLE_管理员')") @PreFilter(value ="filterObject.id%2==0") public List<Users> getTestPreFilter (@RequestBody List<Users> users) { users.forEach(user-> { System.out.println(user.getId() + "\t" + user.getUsername()); }); return users; }
3.7、基于数据库的”记住我” 1、实现思路
使用数据库 + Cookie完成自动认证
认证成功后,向浏览器Cookie中存入一个Token,并向数据库中存入Token和用户信息字符串。 再次进行访问时,获取Cookie中的Token,拿着Cookie中的Token到数据库进行比对,如果能够查询到对应的用户信息,那么认证成功,可以登录。 查看AbstractAuthenticationProcessingFilter
的 successfulAuthentication
方法源码 在认证成功,调用successfulAuthentication
方法时,会执行 rememberMeServices.loginSuccess(request, response, authResult);
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 protected void successfulAuthentication (HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (logger.isDebugEnabled()) { logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult); } SecurityContextHolder.getContext().setAuthentication(authResult); rememberMeServices.loginSuccess(request, response, authResult); if (this .eventPublisher != null ) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this .getClass())); } successHandler.onAuthenticationSuccess(request, response, authResult); }
查看rememberMeServices.loginSuccess(request, response, authResult)
查看 AbstractRememberMeServices
类的loginSuccess
方法
1 2 3 4 5 6 7 8 9 @Override public final void loginSuccess (HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { if (!rememberMeRequested(request, parameter)) { logger.debug("Remember-me login not requested." ); return ; } onLoginSuccess(request, response, successfulAuthentication); }
继续查看 PersistentTokenBasedRememberMeServices
中 onLoginSuccess
的实现 可以看到,此时先是调用 PersistentTokenRepository
的createNewToken
方法生成一个Token,然后再调用addCookie
方法将Token放入Cookie中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 protected void onLoginSuccess (HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { String username = successfulAuthentication.getName(); logger.debug("Creating new persistent login for user " + username); PersistentRememberMeToken persistentToken = new PersistentRememberMeToken( username, generateSeriesData(), generateTokenData(), new Date()); try { tokenRepository.createNewToken(persistentToken); addCookie(persistentToken, request, response); } catch (Exception e) { logger.error("Failed to save persistent token " , e); } }
PersistentTokenRepository
有一个实现类JdbcTokenRepositoryImpl
在这个实现类中,定义了建表语句和对该表的增删改查语句,所以我们可以不用定义“记住我”功能用到的表
1 2 3 4 5 6 7 8 9 10 11 12 13 public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements PersistentTokenRepository { public static final String CREATE_TABLE_SQL = "create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, " + "token varchar(64) not null, last_used timestamp not null)" ; public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?" ; public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)" ; public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?" ; public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?" ; }
查看RemeberMeServices
中的autoLogin
方法 从请求对象中拿出记住我Cookie,判断Cookie是否为空(长度为空或者为null),如果为空,直接返回null,如果长度为0,将Cookie置为空后返回null 如果不为空,那么对token进行解密,根据Token中的信息查询数据库,返回用户对象,然后对用户对象进行校验,如果信息比对无误,则自动登录。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Override public final Authentication autoLogin (HttpServletRequest request, HttpServletResponse response) { String rememberMeCookie = extractRememberMeCookie(request); if (rememberMeCookie == null ) { return null ; } logger.debug("Remember-me cookie detected" ); if (rememberMeCookie.length() == 0 ) { logger.debug("Cookie was empty" ); cancelCookie(request, response); return null ; } UserDetails user = null ; try { String[] cookieTokens = decodeCookie(rememberMeCookie); user = processAutoLoginCookie(cookieTokens, request, response); userDetailsChecker.check(user); logger.debug("Remember-me cookie accepted" ); return createSuccessfulAuthentication(request, user); } catch (CookieTheftException cte) { cancelCookie(request, response); throw cte; } catch (UsernameNotFoundException noUser) { logger.debug("Remember-me login was valid but corresponding user not found." , noUser); } catch (InvalidCookieException invalidCookie) { logger.debug("Invalid remember-me cookie: " + invalidCookie.getMessage()); } catch (AccountStatusException statusInvalid) { logger.debug("Invalid UserDetails: " + statusInvalid.getMessage()); } catch (RememberMeAuthenticationException e) { logger.debug(e.getMessage()); } cancelCookie(request, response); return null ; }
2、具体实现 1 2 3 4 5 6 7 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;
在配置类中注入数据源,并配置操作数据库对象,即注入PersistentTokenRepository
对象(也可以配置JdbcTokenRepositoryImpl
对象) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Autowired private DataSource dataSource; @Bean public PersistentTokenRepository persistentTokenRepository () { JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); jdbcTokenRepository.setCreateTableOnStartup(false ); return jdbcTokenRepository; }
在配置类的 configure(HttpSecurity http)
方法中配置记住我相关信息,并将操作数据库对象放置进去。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Override protected void configure (HttpSecurity http) throws Exception { http.exceptionHandling().accessDeniedPage("/unauth.html" ); http.formLogin() ... .loginPage("/login.html" ) .and() .rememberMe() .tokenRepository(persistentTokenRepository()) .tokenValiditySeconds(60 ) ... }
注意,这里的复选框中的name必须为:remember-me
,这是SpringSecurity
指定的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <form action ="/user/login" method ="post" > 用户名:<input type ="text" name ="username" /> <br /> 密码:<input type ="text" name ="password" /> <br /> <input type ="checkbox" name ="remember-me" /> 自动登录 <br /> <input type ="submit" value ="login" > </form > </body > </html >
3、具体测试 访问 localhost:8111/test/getAll
,点击自动登录,然后进行认证
关掉浏览器,重新打开换后再次输入地址,发现无需认证直接访问到了资源
3.8、用户注销 1、在配置类中添加退出映射路径 1 2 http.logout().logoutUrl("/logout" ).logoutSuccessUrl("/test/index" ).permitAll();
2、测试 设置登录成功后跳转的路径为 success.html
登录成功页面 success.html
如下
1 2 3 4 5 6 7 8 9 10 11 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > 登录成功页面</title > </head > <body > <h1 > 恭喜你,登录成功!</h1 > <a href ="/logout" > 退出</a > </body > </html >
登录成功后,在成功页面点击退出,此时再去访问其他控制器方法,是不能进行访问的。 登录成功页面
此时访问 /test/getAll
,可以访问资源
回到success.html
,点击退出
此时再次访问 /test/getAll
,发现不能访问该资源,自动跳转到登录页
3.9、CSRF 跨站请求伪造 (英语:Cross-site request forgery ),也被称为one-click attack 或者session riding ,通常缩写为 CSRF 或者 XSRF ,是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF利用的是网站对用户网页浏览器的信任。
跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的 浏览器,却不能保证请求本身是用户自愿发出的 。
从Spring Security 4.0开始,默认情况下会启用CSRF保护,以防止CSRF攻击应用 程序,Spring Security CSRF会针对PATCH,POST,PUT和DELETE 方法进行防护,如果是Get请求不保护。
1、开启CSRF保护 1 <input type ="hidden" th:if ="${_csrf}!=null" th:value ="${_csrf.token}" name ="_csrf" />
2、SpringSecurity
防止CSRF
的原理 生成csrfToken
保存到HttpSession
或者Cookie中。
请求到来时,从请求中提取csrfToken
,和保存的csrfToken
做比较,进而判断当前请求是否合法。主要通过CsrfFilter
过滤器来完成。 四、SpringSecurity
微服务权限方案 4.1、什么是微服务 1、由来 微服务最早由Martin Fowler与James Lewis于2014年共同提出,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。
2、优势 微服务每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比 较好解决。 微服务每个模块都可以使用不同的存储方式(比如有的用Redis
,有的用MySQL
等),数据库也是单个模块对应自己的数据库。 微服务每个模块都可以使用不同的开发技术,开发模式更灵活。 3、本质 微服务,关键其实不仅仅是微服务本身,而是系统要提供一套基础的架构,这种架构使得微服务可以独立的部署、运行、升级,不仅如此,这个系统架构还让微服务与微服务之间在结构上“松耦合”,而在功能上则表现为一个统一的整体。这种所谓的“统一的整体”表现出来的是统一风格的界面,统一的权限管理,统一的安全策略,统一的上线过程,统一的日志和审计方法,统一的调度方式,统一的访问入口等等。 微服务的目的是有效的拆分应用,实现敏捷开发和部署。 4.2、微服务认证与授权实现思路 1、认证授权过程分析 如果是基于Session,那么Spring-security
会对cookie里的sessionid
进行解析,找到服务器存储的session信息,然后判断当前用户是否符合请求的要求。 如果是token,则是解析出token,然后将当前请求加入到Spring-security
管理的权限信息中去
如果系统的模块众多,每个模块都需要进行授权与认证,所以我们选择基于token的形式进行授权与认证。
用户输入用户名密码进行认证,认证成功后获取一系列权限列表,然后以用户名为key,权限列表为value的形式存入 Redis
缓存中,并根据用户名相关信息生成Token返回。 浏览器将Token记录在Cookie中,每次调用 API
接口时都默认将Token放在 header 请求头中,并携带着Token进行访问。 SpringSecurity
通过解析header中的token,得到当前用户名后根据用户名就可以从 Redis
中获取权限列表,这样 SpringSecurity
就能够判断当前请求用户是否有权限访问。2、主要实现功能 登录(认证) 添加角色 为角色分配菜单 添加用户 为用户分配角色 4.3、权限管理数据模型 对于一个完整的权限管理模型,至少需要设计5张表
角色与权限(菜单)为多对多关系。
角色与用户为多对多关系。
1、acl_permission
权限表 省略表中数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 CREATE TABLE `acl_permission` ( `id` char(19) NOT NULL DEFAULT '' COMMENT '编号', `pid` char(19) NOT NULL DEFAULT '' COMMENT '所属上级', `name` varchar(20) NOT NULL DEFAULT '' COMMENT '名称', `type` tinyint(3) NOT NULL DEFAULT '0' COMMENT '类型(1:菜单,2:按钮)', `permission_value` varchar(50) DEFAULT NULL COMMENT '权限值', `path` varchar(100) DEFAULT NULL COMMENT '访问路径', `component` varchar(100) DEFAULT NULL COMMENT '组件路径', `icon` varchar(50) DEFAULT NULL COMMENT '图标', `status` tinyint(4) DEFAULT NULL COMMENT '状态(0:禁止,1:正常)', `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除', `gmt_create` datetime DEFAULT NULL COMMENT '创建时间', `gmt_modified` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_pid` (`pid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限';
2、acl_role
角色表 1 2 3 4 5 6 7 8 9 10 CREATE TABLE `acl_role` ( `id` char(19) NOT NULL DEFAULT '' COMMENT '角色id', `role_name` varchar(20) NOT NULL DEFAULT '' COMMENT '角色名称', `role_code` varchar(20) DEFAULT NULL COMMENT '角色编码', `remark` varchar(255) DEFAULT NULL COMMENT '备注', `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除', `gmt_create` datetime NOT NULL COMMENT '创建时间', `gmt_modified` datetime NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
3、acl_role_permission
角色权限关联表 1 2 3 4 5 6 7 8 9 10 11 CREATE TABLE `acl_role_permission` ( `id` char(19) NOT NULL DEFAULT '', `role_id` char(19) NOT NULL DEFAULT '', `permission_id` char(19) NOT NULL DEFAULT '', `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除', `gmt_create` datetime NOT NULL COMMENT '创建时间', `gmt_modified` datetime NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_role_id` (`role_id`), KEY `idx_permission_id` (`permission_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色权限';
4、acl_user
用户表 1 2 3 4 5 6 7 8 9 10 11 12 13 CREATE TABLE `acl_user` ( `id` char(19) NOT NULL COMMENT '会员id', `username` varchar(20) NOT NULL DEFAULT '' COMMENT '微信openid', `password` varchar(32) NOT NULL DEFAULT '' COMMENT '密码', `nick_name` varchar(50) DEFAULT NULL COMMENT '昵称', `salt` varchar(255) DEFAULT NULL COMMENT '用户头像', `token` varchar(100) DEFAULT NULL COMMENT '用户签名', `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除', `gmt_create` datetime NOT NULL COMMENT '创建时间', `gmt_modified` datetime NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
5、acl_user_role
用户角色关联表 1 2 3 4 5 6 7 8 9 10 11 CREATE TABLE `acl_user_role` ( `id` char(19) NOT NULL DEFAULT '' COMMENT '主键id', `role_id` char(19) NOT NULL DEFAULT '0' COMMENT '角色id', `user_id` char(19) NOT NULL DEFAULT '0' COMMENT '用户id', `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除', `gmt_create` datetime NOT NULL COMMENT '创建时间', `gmt_modified` datetime NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_role_id` (`role_id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
6、创建数据库 acldb
执行脚本文件,最终生成的数据库表结构如下
4.4、项目环境搭建 1、项目模块示意图
项目结构示意图
2、引入依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 <properties > <java.version > 1.8</java.version > <mybatis-plus.version > 3.0.5</mybatis-plus.version > <velocity.version > 2.0</velocity.version > <swagger.version > 2.7.0</swagger.version > <jwt.version > 0.7.0</jwt.version > <fastjson.version > 1.2.28</fastjson.version > <gson.version > 2.8.2</gson.version > <json.version > 20170516</json.version > <cloud-alibaba.version > 0.2.2.RELEASE</cloud-alibaba.version > </properties > <dependencyManagement > <dependencies > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-dependencies</artifactId > <version > Hoxton.RELEASE</version > <type > pom</type > <scope > import</scope > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-alibaba-dependencies</artifactId > <version > ${cloud-alibaba.version}</version > <type > pom</type > <scope > import</scope > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > ${mybatis-plus.version}</version > </dependency > <dependency > <groupId > org.apache.velocity</groupId > <artifactId > velocity-engine-core</artifactId > <version > ${velocity.version}</version > </dependency > <dependency > <groupId > com.google.code.gson</groupId > <artifactId > gson</artifactId > <version > ${gson.version}</version > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger2</artifactId > <version > ${swagger.version}</version > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger-ui</artifactId > <version > ${swagger.version}</version > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > <version > ${jwt.version}</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > ${fastjson.version}</version > </dependency > <dependency > <groupId > org.json</groupId > <artifactId > json</artifactId > <version > ${json.version}</version > </dependency > </dependencies > </dependencyManagement > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 <dependencies > <dependency > <groupId > com.hzx</groupId > <artifactId > service_base</artifactId > <version > 1.0.0-SNAPSHOT</version > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</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 > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger2</artifactId > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger-ui</artifactId > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency > <dependency > <groupId > org.apache.velocity</groupId > <artifactId > velocity-engine-core</artifactId > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency > <dependency > <groupId > com.google.code.gson</groupId > <artifactId > gson</artifactId > </dependency > </dependencies > <build > <resources > <resource > <directory > src/main/java</directory > <includes > <include > **/*.xml</include > </includes > <filtering > false</filtering > </resource > </resources > </build >
1 2 3 4 5 6 7 8 9 10 11 <dependencies > <dependency > <groupId > com.hzx</groupId > <artifactId > spring_security</artifactId > <version > 1.0.0-SNAPSHOT</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > </dependency > </dependencies >
service_gateway模块 pom.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <dependencies > <dependency > <groupId > com.hzx</groupId > <artifactId > service_base</artifactId > <version > 1.0.0-SNAPSHOT</version > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-gateway</artifactId > </dependency > <dependency > <groupId > com.google.code.gson</groupId > <artifactId > gson</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency > </dependencies >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <scope > provided </scope > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <scope > provided </scope > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <scope > provided </scope > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger2</artifactId > <scope > provided </scope > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger-ui</artifactId > <scope > provided </scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-pool2</artifactId > <version > 2.6.0</version > </dependency > </dependencies >
spring_security模块 pom.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependencies > <dependency > <groupId > com.hzx</groupId > <artifactId > service_base</artifactId > <version > 1.0.0-SNAPSHOT</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > </dependency > </dependencies >
3、引入工具类 在 service_base
模块中引入工具类
4.5、编写SpringSecurity
认证授权工具类和处理器
1、DefaultPasswordEncoder
密码处理工具类 这个类需要实现 PasswordEncoder
接口,然后重写两个方法
encode加密方法 matches匹配方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class DefaultPasswordEncoder implements PasswordEncoder { public DefaultPasswordEncoder () { this (-1 ); } public DefaultPasswordEncoder (int length) {} @Override public String encode (CharSequence rawPassword) { return MD5.encrypt(rawPassword.toString()); } @Override public boolean matches (CharSequence rawPassword, String encodedPassword) { return encodedPassword.equals(MD5.encrypt(rawPassword.toString())); } }
2、TokenManager
token操作工具类 该工具类包含两个方法,即根据用户名生成token的方法和解析token得到用户名的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Component public class TokenManager { private static final Integer TOKEN_EXPIRATION = 24 * 60 * 60 * 1000 ; public static final String TOKEN_SIGN_KEY = "49ba59abbe56e057" ; public String createToken (String username) { return Jwts.builder().setSubject(username) .setExpiration(new Date(System.currentTimeMillis() + TOKEN_EXPIRATION)) .signWith(SignatureAlgorithm.HS512, TOKEN_SIGN_KEY) .compressWith(CompressionCodecs.GZIP).compact(); } public String getUserInfoFromToken (String token) { return Jwts.parser() .setSigningKey(TOKEN_SIGN_KEY) .parseClaimsJws(token) .getBody().getSubject(); } }
3、TokenLogoutHandler
退出处理器 用户退出时,需要根据token得到用户名,然后根据得到的用户名到Redis
中删除权限列表,然后删除Cookie信息。
这个处理器需要实现LogoutHandler
接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class TokenLogoutHandler implements LogoutHandler { private TokenManager tokenManager; private RedisTemplate redisTemplate; public TokenLogoutHandler (TokenManager tokenManager, RedisTemplate redisTemplate) { this .tokenManager = tokenManager; this .redisTemplate = redisTemplate; } @Override public void logout (HttpServletRequest request, HttpServletResponse response, Authentication authentication) { String token = request.getHeader("token" ); if (StringUtils.isNotBlank(token)) { String username = tokenManager.getUserInfoFromToken(token); redisTemplate.delete(username); } ResponseUtil.out(response, R.ok()); } }
4、UnauthorizedEntryPoint
未授权统一处理 这个类需要实现 AuthenticationEntryPoint
接口,并重写 commence 方法
1 2 3 4 5 6 public class UnauthorizedEntryPoint implements AuthenticationEntryPoint { @Override public void commence (HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ResponseUtil.out(response, R.error()); } }
4.6、编写SpringSecurity
核心过滤器 1、编写 TokenAuthenticationFilter
认证过滤器 这个认证过滤器需要继承 UsernamePasswordAuthenticationFilter
,重写 attemptAuthentication
、 successfulAuthentication
和 unsuccessfulAuthentication
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 public class TokenAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private TokenManager tokenManager; private RedisTemplate<String, Object> redisTemplate; private AuthenticationManager authenticationManager; public TokenAuthenticationFilter (TokenManager tokenManager, RedisTemplate redisTemplate, AuthenticationManager authenticationManager) { this .tokenManager = tokenManager; this .redisTemplate = redisTemplate; this .authenticationManager = authenticationManager; this .setPostOnly(false ); this .setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login" ,"POST" )); } @Override public Authentication attemptAuthentication (HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { User user = new ObjectMapper().readValue(request.getInputStream(), User.class); return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword())); } catch (IOException e) { throw new RuntimeException(); } } @Override protected void successfulAuthentication (HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { SecurityUser user = (SecurityUser) authResult.getPrincipal(); String token = tokenManager.createToken(user.getUsername()); redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(),user.getPermissionValueList()); ResponseUtil.out(response, R.ok().data("token" ,token)); } @Override protected void unsuccessfulAuthentication (HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { ResponseUtil.out(response, R.error()); } }
2、编写 TokenAuthorizationFilter
授权过滤器 这个过滤器需要继承 BasicAuthenticationFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public class TokenAuthorizationFilter extends BasicAuthenticationFilter { private TokenManager tokenManager; private RedisTemplate<String, Object> redisTemplate; public TokenAuthorizationFilter (AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate<String, Object> redisTemplate) { super (authenticationManager); this .tokenManager = tokenManager; this .redisTemplate = redisTemplate; } @Override protected void doFilterInternal (HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { logger.info("=================" + req.getRequestURI()); if (!req.getRequestURI().contains("admin" )) { chain.doFilter(req, res); return ; } UsernamePasswordAuthenticationToken authentication = null ; try { authentication = getAuthentication(req); } catch (Exception e) { ResponseUtil.out(res, R.error()); } if (authentication != null ) { SecurityContextHolder.getContext().setAuthentication(authentication); } else { ResponseUtil.out(res, R.error()); } chain.doFilter(req, res); } private UsernamePasswordAuthenticationToken getAuthentication (HttpServletRequest request) { String token = request.getHeader("token" ); if (StringUtils.isNotBlank(token)) { String username = tokenManager.getUserInfoFromToken(token); List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(username); List<GrantedAuthority> permissions = null ; if (CollectionUtils.isEmpty(permissionValueList)) { permissions = permissionValueList.stream().map(SimpleGrantedAuthority::new ).collect(Collectors.toList()); } return new UsernamePasswordAuthenticationToken(username, token, permissions); } return null ; } }
4.7、编写SpringSecurity
核心配置类 SpringSecurity
的核心配置就是继承 WebSecurityConfigurerAdapter
并添加@EnableWebSecurity
注解
这个配置指明了用户名密码的处理方式、请求路径、登录登出控制等和安全相关的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter { private TokenManager tokenManager; private RedisTemplate<String, Object> redisTemplate; private DefaultPasswordEncoder defaultPasswordEncoder; private UserDetailsService userDetailsService; @Autowired public TokenWebSecurityConfig (TokenManager tokenManager, RedisTemplate<String, Object> redisTemplate, DefaultPasswordEncoder defaultPasswordEncoder, UserDetailsService userDetailsService) { this .tokenManager = tokenManager; this .redisTemplate = redisTemplate; this .defaultPasswordEncoder = defaultPasswordEncoder; this .userDetailsService = userDetailsService; } @Override protected void configure (HttpSecurity http) throws Exception { http.exceptionHandling() .authenticationEntryPoint(new UnauthorizedEntryPoint()) .and().csrf().disable() .authorizeRequests() .anyRequest().authenticated() .and().logout().logoutUrl("/admin/acl/index/logout" ) .addLogoutHandler(new TokenLogoutHandler(tokenManager, redisTemplate)).and() .addFilter(new TokenAuthenticationFilter(tokenManager, redisTemplate, authenticationManager())) .addFilter(new TokenAuthorizationFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic(); } @Override public void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder); } @Override public void configure (WebSecurity web) throws Exception { web.ignoring().antMatchers("/api/**" , "/swagger-ui.html/**" ); } }
4.8、创建 UserDetailsServiceImpl
这个类需要实现 UserDetailsService
,实现 loadUserByUsername
方法。
由于需要查询用户信息和权限信息,所以注入权限Mapper和用户Mapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Service("userDetailsService") public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserService userService; @Autowired private PermissionService permissionService; @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { User user = userService.selectByUsername(username); if (user == null ) { throw new UsernameNotFoundException("用户不存在!" ); } com.hzx.authority.safety.entity.User curUser = new com.hzx.authority.safety.entity.User(); BeanUtils.copyProperties(user,curUser); List<String> permissionList = permissionService.selectPermissionValueByUserId(user.getId()); SecurityUser securityUser = new SecurityUser(); securityUser.setPermissionValueList(permissionList); securityUser.setCurrentUserInfo(curUser); return securityUser; } }
4.9、整合网关 1、配置解决跨域 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration public class CorsConfig { @Bean public CorsWebFilter corsWebFilter () { CorsConfiguration config = new CorsConfiguration(); config.addAllowedMethod("*" ); config.addAllowedOrigin("*" ); config.addAllowedHeader("*" ); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser()); source.registerCorsConfiguration("/**" ,config); return new CorsWebFilter(source); } }
2、创建网关微服务配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 server: port: 8222 spring: application: name: service-gateway cloud: nacos: discovery: server-addr: localhost:8848 gateway: discovery: locator: enabled: true routes: - id: service-acl uri: lb://service-acl predicates: - Path=/*/acl/**
五、原理解读 5.1、请求流程
5.2、认证流程 1、SpringSecurity
过滤器介绍 SpringSecurity
采用的是责任链的设计模式,它有一条很长的过滤器链。
现在对这条过滤器链的 15 个过滤器进行说明:
WebAsyncManagerIntegrationFilter
将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager
进行集成。
SecurityContextPersistenceFilter
在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder
中,然后在该次请求处理完成之后,将SecurityContextHolder
中关于这次请求的信息存储到一个“仓储”中,然后将SecurityContextHolder
中的信息清除,例如在 Session 中维护一个用户的安全信息就是这个过滤器处理的。
用于将头信息加入响应中。
用于处理跨站请求伪造。
用于处理退出登录。
UsernamePasswordAuthenticationFilter
用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login
的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的 usernameParameter
和 passwordParameter
两个参数的值进行修改。
DefaultLoginPageGeneratingFilter
如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
BasicAuthenticationFilter
检测和处理 http basic 认证。
用来处理请求的缓存。
SecurityContextHolderAwareRequestFilter
主要是包装请求对象 request。
AnonymousAuthenticationFilter
检测 SecurityContextHolder
中是否存在Authentication 对象,如果不存在为其提供一个匿名 Authentication。
管理 session 的过滤器
ExceptionTranslationFilter
处理 AccessDeniedException
和 AuthenticationException
异常。
FilterSecurityInterceptor
可以看做过滤器链的出口。
RememberMeAuthenticationFilter
当用户没有登录而直接访问资源时, 从 cookie里找出用户的信息, 如果 Spring Security 能够识别出用户提供的 remember me cookie,用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
2、认证流程 UsernamePasswordAuthenticationFilter
对/login
的POST请求做拦截,校验表单中的用户名和密码。
注意点:
POST请求 表单中用户名和密码的name属性必须为 username
和 password
第一步,过滤方法,判断提交方式是否为POST提交,如果是POST提交,拦截并进行认证,否则直接放行 该过滤器的 doFilter
方法实现在其抽象父类,即 AbstractAuthenticationProcessingFilter
中
第二步,调用子类的方法进行身份认证,认证成功后,将认证信息封装到 Authentication
对象中
第四步,如果在程序运行期间发生了认证错误异常,此时执行 unsuccessfulAuthentication
方法,表示认证失败! 1 2 3 4 5 6 7 8 9 10 11 12 13 14 catch (InternalAuthenticationServiceException failed) { logger.error( "An internal error occurred while trying to authenticate the user." , failed); unsuccessfulAuthentication(request, response, failed); return ; } catch (AuthenticationException failed) { unsuccessfulAuthentication(request, response, failed); return ; }
第五步认证成功后放行,进入过滤器链的下一个过滤器,并执行认证成功方法 successfulAuthentication
1 2 3 4 5 6 7 if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } successfulAuthentication(request, response, chain, authResult);
3、认证细节 在上面的第二步中,会调用子类 UsernamePasswordAuthenticationFilter
的方法进行认证,查看源码 如果请求方式不是 POST
,则抛出异常 获取请求对象携带的用户名和密码
使用传入的用户名密码构造 Authentication
对象,并标记该对象未认证 将请求中一些属性信息放入 Authentication 对象中,如remoteAddress
、sessionId
调用 ProviderManager
对象的 authenticate
方法进行身份认证,此时会调用我们自己写的 UserDetailsService
查询数据库。 1 2 3 4 5 6 7 8 9 username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); setDetails(request, authRequest); return this .getAuthenticationManager().authenticate(authRequest);
查看 UsernamePasswordAuthenticationToken
对象的构建过程 UsernamePasswordAuthenticationToken
是Authentication 接口的实现类,该类有两个构造器,一个用于封装前端请求传入的未认证的用户信息,一个用于封装认证成功后的用户信息:
用于封装未认证用户信息的构造器如下
1 2 3 4 5 6 7 public UsernamePasswordAuthenticationToken (Object principal, Object credentials) { super (null ); this .principal = principal; this .credentials = credentials; setAuthenticated(false ); }
用于封装已认证用户信息的构造器如下
1 2 3 4 5 6 7 8 public UsernamePasswordAuthenticationToken (Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super (authorities); this .principal = principal; this .credentials = credentials; super .setAuthenticated(true ); }
Authentication
接口的实现类用于存储用户认证信息,查看该接口具体定义:
5.3、权限访问流程 在权限访问流程中,主要用到 ExceptionTranslationFilter
和FilterSecurityInterceptor
1、ExceptionTranslationFilter
该过滤器是用于处理异常的,不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。
2、FilterSecurityInterceptor
FilterSecurityInterceptor
是过滤器链的最后一个过滤器,该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,最终所抛出的异常会由前一个过滤器ExceptionTranslationFilter
进行捕获和处理。
需要注意,Spring Security 的过滤器链是配置在 Spring MVC
的核心组件DispatcherServlet
运行之前。也就是说,请求通过 Spring Security 的所有过滤器,不意味着能够正常访问资源,该请求还需要通过 SpringMVC
的拦截器链。