当前位置:   article > 正文

开发笔记 | 认证授权+Spring Security+OAuth2快速学习笔记_配置 oauth2的公私密钥对,放到admin目录下

配置 oauth2的公私密钥对,放到admin目录下

本文仅为个人学习笔记,通过简单的框架搭建来初步学习Spring Security及OAuth2,文中部分方法注解已过时,但仅为学习使用

基础概念

认证:认证用户的合法性,如登录

授权:登录后,用户具有什么操作权限

会话:将用户认证信息保存起来,避免重复认证(常见方式session,token等)

整体的思想:登录认证获得一个权限标识与权限相关信息,存入会话中(session,结合redis等),使得在有效期内无需重复登录或者请求接口校验用户的权限

RBAC:Role-Based Access Control 基于角色的访问控制,由用户,角色,权限组成

简单来说就是一个用户,具有什么角色,每个角色具有什么权限,从而决定改用户具有什么权限

基于RBAC的简单例子

基础代码及登录界面

pom增加依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.projectlombok</groupId>
  7. <artifactId>lombok</artifactId>
  8. <optional>true</optional>
  9. </dependency>

yml配置文件

  1. server:
  2. port: 5555
  3. spring:
  4. application:
  5. name: security-demo
  6. thymeleaf:
  7. prefix: classpath:templates/
  8. suffix: .html

实体

  1. @Data
  2. @AllArgsConstructor
  3. @NoArgsConstructor
  4. public class User {
  5. private String userId;
  6. private String userName;
  7. private String password;
  8. private List<Role> roles = new ArrayList();
  9. }
  10. @Data
  11. @AllArgsConstructor
  12. @NoArgsConstructor
  13. public class Role {
  14. private String roleId;
  15. private String roleName;
  16. private List<Resource> resources = new ArrayList();
  17. }
  18. @Data
  19. @AllArgsConstructor
  20. @NoArgsConstructor
  21. public class Resource {
  22. private String resourceId;
  23. private String resourceName;
  24. private String resourceType;
  25. }

controller

  1. @Controller
  2. @RequestMapping("/auth")
  3. public class LoginController {
  4. @Resource
  5. private AuthService authService;
  6. @GetMapping
  7. public String test(){
  8. return "success";
  9. }
  10. //首页
  11. @RequestMapping("/index")
  12. public String index(){
  13. return "index";
  14. }
  15. //登录
  16. @RequestMapping("/login")
  17. public String login(String userName,String password, HttpServletRequest request){
  18. User query = new User();
  19. query.setUserName(userName);
  20. query.setPassword(password);
  21. User userResult = this.authService.getUser(query);
  22. if(null == userResult){
  23. return "failed";
  24. }
  25. request.getSession().setAttribute("current_user",userResult);
  26. return "success";
  27. }
  28. //获取当前用户
  29. @PostMapping("/getCurrUser")
  30. public User getCurrentUser(HttpSession session){
  31. return (User)session.getAttribute("current_user");
  32. }
  33. //登出
  34. @PostMapping("logout")
  35. public ResponseEntity logout(HttpSession session){
  36. session.removeAttribute("current_user");
  37. return ResponseEntity.ok("已退出登录");
  38. }
  39. }

service

  1. @Service
  2. public class AuthService {
  3. @Resource
  4. private AuthMapper authMapper;
  5. //查询用户
  6. public User getUser(User user){
  7. User userResult = this.authMapper.getUser(user);
  8. return userResult;
  9. }
  10. }

mapper模拟数据库

  1. @Component
  2. public class AuthMapper {
  3. //模拟数据库
  4. List<User> userDataBase = Arrays.asList(new User("1","admin","123456",null,null));
  5. public User getUser(User user){
  6. List<User> list = userDataBase.stream().filter(e -> e.getUserName().equals(user.getUserName()) && e.getPassword().equals(user.getPassword()))
  7. .collect(Collectors.toList());
  8. return list.size() > 0 ? list.get(0) : null;
  9. }
  10. }

简单界面

resources/templates新增界面

登录界面

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>登录页面</title>
  6. </head>
  7. <body>
  8. <form action="/auth/login" method="post">
  9. 用户名:<input type="text" name="userName" required><br/>
  10. 密 码:<input type="text" name="password" required><br/>
  11. <input type="submit" value="登录"> <input type="reset" value="重置">
  12. </form>
  13. </body>
  14. </html>

登录成功界面

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>登录成功</title>
  6. </head>
  7. <body>
  8. 登录成功
  9. </body>
  10. </html>

登录失败界面

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>失败</title>
  6. </head>
  7. <body>
  8. 账号或者密码失败
  9. </body>
  10. </html>

到此可以启动项目,输入账号密码,界面正常跳转,到此步认证完成

整合权限控制

修改登录成功后的界面

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>登录成功</title>
  6. </head>
  7. <body onload="lordData();">
  8. 登录成功
  9. <div>当前用户:</div>
  10. <div id="currentUser"></div>
  11. <br/>
  12. <div>获取苹果:</div>
  13. <div id="getApple"></div>
  14. <div>获取香蕉:</div>
  15. <div id="getBanana"></div>
  16. <br/>
  17. <div onclick="logout();">退出登录</div>
  18. <br/>
  19. <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
  20. <script type="text/javascript">
  21. function lordData(){
  22. currentUser();
  23. getApple();
  24. getBanana();
  25. }
  26. function logout(){
  27. $.ajax({
  28. //请求方式
  29. type : "POST",
  30. //请求的媒体类型
  31. // contentType: "application/json;charset=UTF-8",
  32. //请求地址
  33. url : "/auth/logout",
  34. //数据,json字符串
  35. data : {},
  36. //请求成功
  37. success : function(result) {
  38. //result = JSON.stringify(result);
  39. alert(result)
  40. },
  41. //请求失败,包含具体的错误信息
  42. error : function(error){
  43. alert(error)
  44. }
  45. });
  46. }
  47. function currentUser() {
  48. $.ajax({
  49. type : "POST",
  50. url : "/auth/getCurrUser",
  51. data : {},
  52. success : function (result) {
  53. result = JSON.stringify(result);
  54. $('#currentUser').html("success:" + result);
  55. },
  56. error : function (error) {
  57. $('#currentUser').html("error:" + error);
  58. }
  59. })
  60. }
  61. function getApple() {
  62. $.ajax({
  63. type : "GET",
  64. url : "/apple",
  65. data : {},
  66. success : function (result) {
  67. $('#getApple').html("success:" + result);
  68. },
  69. error : function (error) {
  70. $('#getApple').html("error:" + error);
  71. }
  72. })
  73. }
  74. function getBanana() {
  75. $.ajax({
  76. type : "GET",
  77. url : "/banana",
  78. data : {},
  79. success : function (result) {
  80. //result = JSON.stringify(result);
  81. $('#getBanana').html("success:" + result);
  82. },
  83. error : function (error) {
  84. $('#getBanana').html("error:" + error);
  85. }
  86. })
  87. }
  88. </script>
  89. </body>
  90. </html>

新增两个资源获取接口

  1. @RestController
  2. @RequestMapping("/apple")
  3. public class AppleController {
  4. @GetMapping
  5. public String getApple(){
  6. return "资源1苹果";
  7. }
  8. }
  9. @RestController
  10. @RequestMapping("/banana")
  11. public class BnanaController {
  12. @GetMapping
  13. public String getBanana(){
  14. return "资源2香蕉";
  15. }
  16. }

启动项目,登录后,界面会去请求对应的接口获得数据

但此时接口均为做权限的限制,故通过定义适配器+拦截器进行权限的拦截

完善模拟数据库的权限数据

登录成功后,将用户及其权限信息存入session

创建苹果,香蕉资源,再创建管理员角色(苹果,香蕉资源)和苹果经销商角色(苹果资源),

再创建管理员用户(管理员角色(苹果,香蕉资源)),只卖苹果商家用户(苹果经销商角色(苹果资源))

  1. @Component
  2. public class AuthMapper {
  3. public User getUser(User user){
  4. List<User> list = this.getUserList().stream().filter(e -> e.getUserName().equals(user.getUserName()) && e.getPassword().equals(user.getPassword()))
  5. .collect(Collectors.toList());
  6. return list.size() > 0 ? list.get(0) : null;
  7. }
  8. //模拟用户数据库
  9. public List<User> getUserList(){
  10. List<User> users = new ArrayList<>();
  11. //两个资源呢数据
  12. Resource appleResource = new Resource("1","apple","1");
  13. Resource bananaResource = new Resource("2","banana","1");
  14. //创建三种角色 管理员 苹果卖家
  15. //管理员角色具有苹果 香蕉数据查看权限
  16. Role adminRole = new Role("1","admin",Arrays.asList(appleResource,bananaResource));
  17. //苹果卖家只能看到苹果数据的权限
  18. Role appleRole = new Role("2","appleRole",Arrays.asList(appleResource));
  19. //创建两个用户 超级管理员具有admin角色
  20. User admin = new User("1","admin","123456",Arrays.asList(adminRole));
  21. users.add(admin);
  22. //苹果用户 具有 appleRole角色
  23. User appleOnly = new User("1","apple","123456",Arrays.asList(adminRole));
  24. users.add(appleOnly);
  25. return users;
  26. }
  27. }

创建应用上下文配置MyWebAppConfigurer

(在此处可配置接口的拦截器)

  1. @Component
  2. public class MyWebAppConfigurer implements WebMvcConfigurer {
  3. @Resource
  4. private AuthInterceptor authInterceptor;
  5. //启动界面的简单配置
  6. @Override
  7. public void addViewControllers(ViewControllerRegistry registry) {
  8. //重定向至主页
  9. registry.addViewController("/").setViewName("redirect:/index.html");
  10. }
  11. //配置权限拦截器
  12. @Override
  13. public void addInterceptors(InterceptorRegistry registry) {
  14. registry.addInterceptor(authInterceptor).addPathPatterns("/**");
  15. }
  16. }

创建拦截器

对登录状态及资源的范问权限进行拦截

  1. @Component
  2. public class AuthInterceptor extends HandlerInterceptorAdapter {
  3. @Override
  4. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  5. //设置不需要登录即刻访问的接口
  6. String url = request.getRequestURI();
  7. if (url.contains(".") || url.startsWith("/auth/")){
  8. return true;
  9. }
  10. //未登录的用户
  11. if(null == request.getSession().getAttribute("current_user")){
  12. response.setCharacterEncoding("UTF-8");
  13. response.setHeader("content-type","text/html;charset=UTF-8");
  14. response.getWriter().write("请先登录!");
  15. return false;
  16. }else {
  17. //已登录的用户 进行资源的权限校验
  18. User user = (User)request.getSession().getAttribute("current_user");
  19. if(url.startsWith("/apple") && this.hasPermission("apple",user.getRoles())){
  20. return true;
  21. }else if (url.startsWith("/banana") && this.hasPermission("banana",user.getRoles())){
  22. return true;
  23. }else {
  24. response.setCharacterEncoding("UTF-8");
  25. response.setHeader("content-type","text/html;charset=UTF-8");
  26. response.getWriter().write("暂无权限");
  27. return false;
  28. }
  29. }
  30. }
  31. public boolean hasPermission(String type,List<Role> roles){
  32. for (Role r : roles){
  33. if(r.getResources().stream().filter(e -> e.getResourceName().equals(type)).count() > 0){
  34. return true;
  35. }
  36. }
  37. return false;
  38. }
  39. }

至此将对没登录,登陆后不同用户根据权限进行接口访问限制

admin登录

 apple登录

 未登录访问http://127.0.0.1:5555/banana

 至此一个简单的基于RBAC模型的例子完成,下一步结合Spring Security来优化完善。

Spring Security

配置

pom

创建项目,新增依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-security</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>org.projectlombok</groupId>
  11. <artifactId>lombok</artifactId>
  12. <optional>true</optional>
  13. </dependency>

yml

Spring Security不需要配置即可直接启动

  1. server:
  2. port: 5556
  3. spring:
  4. application:
  5. name: spring-security-demo
  6. thymeleaf:
  7. prefix: classpath:templates/
  8. suffix: .html

启动类加上@EnableWebSecurity

controller

同上一个例子一样的苹果,香蕉资源

  1. @RestController
  2. @RequestMapping("/apple")
  3. public class AppleController {
  4. @GetMapping
  5. public String getApple(){
  6. return "资源1苹果";
  7. }
  8. }
  9. @RestController
  10. @RequestMapping("/banana")
  11. public class BnanaController {
  12. @GetMapping
  13. public String getBanana(){
  14. return "资源2香蕉";
  15. }
  16. }

此时启动项目,访问这两个接口,均跳转Spring Security自带的登录界面,输入user,密码为控制台日志打印的【Using generated security password】即可登录

认证与授权的实现

修改建应用上下文配置,创建默认用户及权限

通过自定义注入UserDetailsService 用于管理系统的用户账号密码信息,如果不注入,springt security将默认注入一个包含登录名为user的用户(如上),密码打印在控制台

PasswordEncoder密码解析器

spring security要求容器内需要拥有一个PasswordEncoder实例

常用BCryptPasswordEncoder实现类,同一个字符串每次加密得到不同结果

构成:

$2a表示版本 $10 表示算法强度 $xxx 随机盐 xxxx 文本hash值

60位字符串

  1. @Configuration
  2. public class MyWebAppConfigurer implements WebMvcConfigurer {
  3. //启动界面的简单配置
  4. @Override
  5. public void addViewControllers(ViewControllerRegistry registry) {
  6. //此处修改为重定向至 /login接口 /login由spring security提供
  7. registry.addViewController("/").setViewName("redirect:/login");
  8. }
  9. //免密解析器
  10. @Bean
  11. public PasswordEncoder passwordEncoder(){
  12. return new BCryptPasswordEncoder(10);
  13. }
  14. /**
  15. * 设置初始用户来源
  16. * 自行注入一个UserDetailsService UserDetailsServiceAutoConfiguration中有默认的
  17. * InMemoryUserDetailsManager
  18. */
  19. @Bean
  20. public UserDetailsService userDetailsService(){
  21. InMemoryUserDetailsManager manager =new InMemoryUserDetailsManager(
  22. User.withUsername("admin").password(passwordEncoder().encode("admin")).authorities("apple","salary","ROLE_stu").build(),
  23. User.withUsername("apple").password(passwordEncoder().encode("apple")). authorities("apple").build(),
  24. User.withUsername("banana").password(passwordEncoder().encode("banana")).authorities("banana").build());
  25. return manager;
  26. }
  27. }

其中,在设置权限时候,用“ROLE_” + 角色表示角色权限,此时ROLE_不能省略

如配置了角色ROLE_stu,在spring security配置文件中,设置角色权限

.antMatchers("/test/**").hasRole("stu")即可,此时也不能带上"ROLE_"

配置WebSecurityConfig配置

  1. @EnableWebSecurity
  2. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  3. //定义安全拦截策略
  4. @Override
  5. protected void configure(HttpSecurity httpSecurity) throws Exception{
  6. httpSecurity.csrf().disable() //关闭csrf跨站攻击防御
  7. .authorizeRequests()
  8. .antMatchers("/apple/**").hasAuthority("apple") // 配置拦截路径及资源权限
  9. .antMatchers("/banana/**").hasAuthority("banana")
  10. .antMatchers("/auth/**").permitAll() //登录相关接口不进行拦截
  11. .anyRequest().authenticated() //其他请求都需要登录
  12. .and()
  13. .formLogin().defaultSuccessUrl("/auth/success").failureForwardUrl("/auth/failed"); //登录成功/失败跳转的页面
  14. }
  15. }

修改登录controller

  1. @Controller
  2. @RequestMapping("/auth")
  3. public class LoginController {
  4. //登录成功失败界面
  5. @GetMapping("/success")
  6. public String success(){
  7. return "success";
  8. }
  9. @PostMapping("/failed")
  10. public String failed(){
  11. return "failed";
  12. }
  13. //首页
  14. @RequestMapping("/index")
  15. public String index(){
  16. return "index";
  17. }
  18. //获取当前用户
  19. @PostMapping("/getCurrUser")
  20. @ResponseBody
  21. public Object getCurrentUser(HttpSession session){
  22. return session.getAttribute("current_user");
  23. }
  24. //登出
  25. @PostMapping("logout")
  26. @ResponseBody
  27. public ResponseEntity logout(HttpSession session){
  28. session.removeAttribute("current_user");
  29. return ResponseEntity.ok("已退出登录");
  30. }
  31. /**
  32. * 获取当前用户的多种方式
  33. * @param principal
  34. * @return
  35. */
  36. @GetMapping("/getUserByPrincipal")
  37. public String getUserByPrincipal(Principal principal){
  38. return principal.getName();
  39. }
  40. @GetMapping(value = "/getLoginUserByAuthentication")
  41. public String currentUserName(Authentication authentication) {
  42. return authentication.getName();
  43. }
  44. @GetMapping(value = "/username")
  45. public String currentUserNameSimple(HttpServletRequest request) {
  46. Principal principal = request.getUserPrincipal(); return principal.getName();
  47. }
  48. // @GetMapping("/getLoginUser")
  49. // public String getLoginUser(){
  50. // User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal() ;
  51. // return user.getUsername();
  52. // }
  53. }

将首页/登录成功页/失败页复制过来

修改首页中获取当前用户部分

  1. function currentUser() {
  2. $.ajax({
  3. type : "GET",
  4. url : "/auth/getUserByPrincipal",
  5. data : {},
  6. success : function (result) {
  7. result = JSON.stringify(result);
  8. $('#currentUser').html("success:" + result);
  9. },
  10. error : function (error) {
  11. $('#currentUser').html("error:" + error);
  12. }
  13. })
  14. }

至此简单的spring security项目准备完毕,但是此时用户信息及权限规则,均通过UserDetailsService写至内存中,实际项目需要结合数据库来获取用户权限数据

测试

启动项目-》登录前,直接请求/banana接口,发现均会跳转至自带登录界面,用banana/banana登录后,正常获取banana资源,而获取不到apple资源

 直接访问http://127.0.0.1:5556/banana

 正常获取数据,访问http://127.0.0.1:5556/apple

返回403无法访问

使用自带/logout接口退出当前用户后,访问/banana接口,则此时跳转至登录界面

Spring Security拓展点

数据源

SpringSecurity通过引用Spring容器的UserDetailsService来管理用户数据,默认情况下会提供一个为user的用户,可通过自定义注入改变用户数据,但实际开发中,用户数据及权限配置信息来源于数据库。在SpringSecurity提供JdbcUserDetailsManager对数据库的用户数据进行管理

自定义登录界面整合数据库权限管理

修改配置从数据库获取用户及权限

修改UserDetails,从数据库加载用户信息

修改WebSecurityConfig中httpSecurity,改成从数据库加载授权配置

密码解析器

SpringSecurity提供多种密码解析器,CryptPassEncoder/Argon2PasswordEncoder等,接实现PassEncoder接口,最常见为BCryptPasswordEncoder

自定义授权等配置

自定义拦截配置

配置类WebSecurityConfig继承WebSecurityConfigurerAdapter,通过重写的configure(HttpSecurity httpSecurity)进行拦截规则配置,包括访问控制,登录登出界面设置等

自定义登录

通过配置HttpSecurity中的方法来自定义

loginPage()配置登录页面

如在WebSecurityConfig中新增.loginPage("/auth/index")

当输入http://127.0.0.1:5556/login

页面跳转为自定义界面 http://127.0.0.1:5556/auth/index

此时需要修改登录界面index.html 及登录请求接口的逻辑

修改index.html中action 编写/auth/tologin接口用于处理登录逻辑,自定义后,将不会使用UserDetailsService来进行处理

  1. <form action="/auth/tologin" method="post">
  2. 用户名:<input type="text" name="userName" required><br/>
  3. 密 码:<input type="text" name="password" required><br/>
  4. <input type="submit" value="登录"> <input type="reset" value="重置">
  5. </form>

loginProcessingUrl() 设置登录逻辑

remember-me

登录时提交remember-me参数 on/yes/1/true 则会记住当前用户的token至cookie中(默认失效时间2周.tokenValiditySeconds(60)设置失效时间)

httpSecurity.rememberMe().rememberMeParameter("remember-me") //登录时,前端传参的名称

安全拦截

httpSecurity..authorizeRequests().antMachers()设置请求路径匹配

antMachers().permitAll()允许所有人访问,

antMachers().denyAll()所有人拒绝访问,

antMachers().anonymous()未登录可以访问,已登录不可访问

antMachers().hasAuthority()/hasRole() 配置需要有对应的权限/角色才能访问

跨域配置csrf

Cross-Site Request Forgery 跨站点请求伪造,一种安全攻击手段,利用客户端信息伪装成正常用户进行攻击。

当打开csrf配置后,SpringSecurity提供CsrfFilter对csrf参数进行检查,每次请求,session中加入_csrf的token,每次带上token,SpringSecurity进行检查

异常处理

@ControllerAdvice注入一个异常处

理类,以@ExceptionHandler注解声明方法,往前端推送异常信息。

结合注解的使用

权限控制注解

以下四个注解默认不生效,需要在配置类(MyWebAppConfigurer)或者启动类加上@EnableGlobalMethodSecurity(prePostEnabled = true)开启

@PreAuthorize("hashRole(‘admin’)") 执行方法前判断是都具有该角色权限

注解支持ROLE_admin/admin均可,但是配置类中.antMatchers("/test/**").hasRole("stu")不能带上"ROLE_"

@PostAuthorize("returnObject.name == authentication.name") 方法执行后,判断返回的值是否与认证主体中的某个值相等,若相等则返回,否则抛出异常

@PreFilter(filterTarget=”a“, value=”filterObject%2==0“) 方法执行前,过滤参数a 当a%2不等于0则会被过滤,不会传入方法中(入参a 为List<Integer> a)

@PostFilter("filterObject.name == authentication.name“) 方法执行后根据条件过滤结果

角色控制注解

@EnableGlobalMethodSecurity(securedEnable = true)开启。

@Secured("ROLE_stu") 作用于方法,类,判断是否具备该角色(角色严格区分大小写)

会话控制

获取当前用户

通过认证后,SpringSecurity提供会话管理,保存用户的认证信息,以避免每次操作都要进行认证操作。认证通过后,身份信息将放入SecurityContextHolder上下文,SecurityContext与当前线程绑定,用于方便获取用户身份

通过SecurityContextHolder.getContext().getAuthentication()获取当前用户登录信息

  1. /**
  2. * 获取当前用户的多种方式
  3. * @param principal
  4. * @return
  5. */
  6. @GetMapping("/getUserByPrincipal")
  7. @ResponseBody
  8. public String getUserByPrincipal(Principal principal){
  9. return principal.getName();
  10. }
  11. @GetMapping(value = "/getLoginUserByAuthentication")
  12. @ResponseBody
  13. public String currentUserName(Authentication authentication) {
  14. return authentication.getName();
  15. }
  16. @GetMapping(value = "/username")
  17. @ResponseBody
  18. public String currentUserNameSimple(HttpServletRequest request) {
  19. Principal principal = request.getUserPrincipal(); return principal.getName();
  20. }
  21. @GetMapping(value = "/username")
  22. @ResponseBody
  23. public String getUserByContext() {
  24. Principal principal = (Principal)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
  25. return principal.getName();
  26. }

 会话控制

 通过配置sessionCreationPolicy参数来管理Session

  1. @EnableWebSecurity
  2. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  3. //定义安全拦截策略
  4. @Override
  5. protected void configure(HttpSecurity httpSecurity) throws Exception{
  6. httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
  7. .and()
  8. ......

 策略包含

always 如果无Session则创建一个

if required 如果需要登录时创建一个(默认)

never SpringSecurity不会创建Session,如果应用其他地方创建,则会被使用

stateless SpringSecurity绝不创建也不使用Session

会话超时

在yml文件中直接设置session过期时间

server.servlet.session.timeout = 3000s (springsecurity默认一分钟 小于一分钟也会按照一分钟失效处理)

session过期后可通过SpringSecurity设置跳转界面

httpSecurity . sessionManagement ()
    . expiredUrl ( "/auth/index ?error=EXPIRED_SESSION" ) //session过期后跳转
    . invalidSessionUrl ( "/auth/index ?error=INVALID_SESSION" ); //传入无效sessionId跳转

 同账号多端登录前者被踢下线功能

httpSecurity.sessionManagement()

    .maximumSessions(1)  //设置某段时间内允许几个端登录

    .maxSessionsPreventsLogin(false) //true表示已经登录则不允许再次登录 false允许再次登录但前者会被踢下线

     .expiredSessionStrategy(new CustomExpiredSessionStrategy()) //自定义被踢下线后的操作 需要实现自定义策略

public class CustomExpiredSessionStrategy implements SessionInformationExpiredStrategy{

    private ObjectMapper  objectMapper = new ObjectMapper();

    @override

    public void onExpiredSessionDetected(SessionInformationExpiredEvent event){

       Map<String, Object> map = new HashMap();

       map.put("code",403);

       map.put("msg","您已在其他地方登录,被迫下线" + event.getSessionInformation().getLsatRequest());

       String json = objectMapper.writeValueAsString(map);

       event.getResponse().setContentType("application/json;charset=utf-8");

       event.getResponse().getWriter().write(json);

    }

}

通过两个不同的浏览器进行同账号登录测试

安全会话cookie

在yml文件中直接设置

server.servlet.session.cookie.http only = true # true 浏览器脚本无法访问cookie
server.servlet.session.cookie.secure = true # true cookie仅通过Https发送

退出登录

SpringSecurtity提供退出接口/logout 跳转至登出界面,可以直接调用

也可在WebSecurityConfig中自定义退出界面/退出后跳转地址

  1. .and()
  2. .logout() //开启自定义退出,使用WebSecurityConfigurerAdapter 会自动被应用 .logoutUrl("/logout") //默认退出地址
  3. .logoutSuccessUrl("/auth/logout") //退出后的跳转地址
  4. .addLogoutHandler(new SecurityContextLogoutHandler()) //添加LogoutHandler,负责退出时的清理工作.默认 SecurityContextLogoutHandler会被添加为最后一个LogoutHandler
  5. .invalidateHttpSession(true); //指定是否在退出时让HttpSession失效,默认是true

退出时,会进行的操作(SecurityContextLogoutHandler)

1.http session失效

2.清除SecurityContext上下文

3.跳转至退出后的地址

SecurityContextLogoutHandler退出时负责SecurityContext的清理工作

分布式系统的认证方案

单体系统演变至多服务,多个服务利用一套独立的第三方系统提供统一的认证授权

统一认证授权

独立的认证服务,用于统一处理认证授权,不管是不同类型的用户,不同类型的客户端(web,h5,小程序,app等),采用一致的认证授权会话处理机制,实现统一登录认证授权

需要支持多种认证方式:账号密码,短信验证,二维码,人脸识别等灵活切换

多重认证场景

购物,支付需要不同安全等级需要对应认证场景

应用接入认证

开放部分API给第三方使用。内部与外部第三方采用统一的

认证方案(基于session与token两种方案)

方案1基于session的认证方式

在分布式中,将session信息同步至各个服务,并对请求进行负载均衡

做法:

1.进行session复制:多个服务器之间同步session,使得session保持一致,但是对外透明

2.session黏贴:用户访问服务器集群中某台服务器后,后续所有请求都需要落到该服务器上

3.session集中存储:将session存入分布式缓存中,所有服务器均从分布式缓存中获取session信息

优点:更好的在服务端进行会话控制,安全性较高

缺点:但是客户端需要存储sessionId,对不同的客户端不能有效使用等

方案2基于token的认证方式

生成认证的token存储,每次请求携带并进行校验

选择方案

统一认证服务(UAA)和网关结合来进行认证授权

uaa负责接入方认证,登录用户认证,授权,令牌管理,完成实际的用户认证授权

API网关作为整个分布式系统的统一入口,进行身份认证,监控,负载均衡等


OAuth2.0

前言

OAuth开放授权标准,允许用户授权第三方应用B获取在微信上注册的信息,从而不需要向B提供微信账号密码,避免B获取用户在微信上的所有数据内容。OAuth协议用于保证双方的可信。

认证流程

用户在B上选择通过微信登录-》弹出微信登录方式(账号密码/二维码等)-》选择弹出的微信同意登录-》B获取用户微信账号信息-》B新建账号与微信绑定-》用B账号进行登录

一个应用要求通过OAuth授权,需要现在对方网站进行登记,这样在请求的时候对方才能知道谁在请求

OAuth2中的角色

客户端Client:如浏览器,微信客户端,不存储资源,需要通过资源拥有者授权去请求资源服务器的资源

资源拥有者Resource Owner:用户,也可以是应用程序,表示某个资源的拥有者

授权服务器Authorization Server:认证服务器,如微信,服务提供者对资管拥有者的身份认证,对访问资源进行授权,认证成功给客户端发放令牌(access_token)作为凭证

资源服务器Resource Server:如微信服务器,B服务器,通过OAuth协议让B获取用户在微信上的用户信息,B同时通过OAuth协议让用户访问自己的资源

clientDetails:cilent_id客户信息,代表B在微信中的唯一索引
secret :密钥,B获取微信的信息需要提供加密字段
scope:授权作用域,代表B获取微信的信息访问
access_token:授权码,B获取微信用户信息的凭证,如微信的接口调用凭证
grant_type:授权类型,如微信支持基于授权码的authorization_code模式,OAuth提供多种授权方式
userDetails:授权用户标识user_id,如微用户在微信中微信号

Spring Security OAuth2.0

基于OAuth2协议的服务实现框架,OAuth2包含 授权(认证)服务资源服务,可在同一个应用中实现两个服务或者拆分多个应用实现。

授权服务(Authorization Server)

包含接入端及用户登入的合法性验证并发放token等,配置一个认证服务必须具有的endpoints
AuthorizationEndpoint 服务用于认证请求。默认URL:/oauth/authorize
TokenEndpoint 服务用于访问令牌的请求。默认URL:/oauth/token
OAuth2AuthenticationProcessingFilter 用于对请求方给出的身份令牌进行解析鉴权

认证流程

1.客户请求server-uaa服务申请access_token授权码

2.客户携带access_token访问server-b服务

3.server-b校验access_token合法性,若合法返回资源信息

OAuth2简单运用实例(以码云为资源/认证服务器)

描述:通过码云实现模拟第三方登录(授权码模式)

客户端及第三方应用创建

1.新建简单项目,创建个简单回调页面(客户端)

2.在码云上注册应用,获取Client ID(客户端id),Client Secret(密钥)等(授权服务器,码云同为资源服务器)

上面说过一个应用要求通过OAuth授权,需要现在对方网站进行登记,这样在请求的时候对方才能知道谁在请求,所以此处创建第三应用可以理解为,在码云上登记一个客户端信息

3.点击模拟请求进行测试-》同意授权

4.页面跳转至我们设置的回调界面,在链接上带回一个code

 5.查看码云OAuth文档

Gitee OAuth 文档https://gitee.com/api/v5/oauth_doc

通过授权码模式的认证授权

请求的几个参数

1.clientDetails(client_id) 客户信息,第三方应用id,项目在码云中的唯一索引

2.secret 密钥,代表获取码云信息需要提供的加密字段(与码云加密算法有关)

3.scope 授权作用域,可以获取码云信息的范围

4.access_token 授权码,允许获取码云信息的凭证

5.grant_type 四种授权类型

1.通过请求,进行授权模拟(授权码模式)

.根据文档,通过浏览器将用户引导至码云第三方认证页面(GET)

https://gitee.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code

 带入实际参数client_id,redirect_uri,response_type

https://gitee.com/oauth/authorize?client_id=c60b65bee5e46c7835ef999e9726b81b18e0ae69e39f25e80ede4df45355278f&redirect_uri=http://127.0.0.1:8888/callback.html&response_type=code

2.出现第3步,第四步相同操作

3. 将授权码code向码云认证服务器发送POST请求,获取access_token(有效期一天)

https://gitee.com/oauth/token?grant_type=authorization_code&code=c59e19acf5eb78211ca4246d21cd652cbba0e3124d86c7d9ea4deeecd1e9d513&client_id=c60b65bee5e46c7835ef999e9726b81b18e0ae69e39f25e80ede4df45355278f&redirect_uri=http://127.0.0.1:8888/callback.html&client_secret=9548b96604a5ab1a50b201c09065a2e0c84e95be3c4163704ee29341080afcd5

当access_token失效后,可以通过refresh_token重新获取access_token(POST)

https://gitee.com/oauth/token?grant_type=refresh_token&refresh_token={refresh_token}

 OAuth2协议包含的角色

1.客户端:例子中创建的客户端服务,本身无资源,通过浏览器去获取码云资源,及需要通过资源拥有者的授权去资源服务器获取资源

2.资源拥有者,例子中拥有码云账号的用户,资源拥有者

3.授权服务器(认证服务器),例子中码云认证服务器,对资源拥有者身份认证授权,认证成功发放令牌(access_token)作为客户端访问资源服务器凭证

4.资源服务器,例子中码云,存储资源的云服务器,通过OAuth协议,用户可以获取码云账户数据,仓库数据,代码等资源

OAuth2协议的四种授权类型(grant_type)

授权码模式authorization-code

用户访问客户端,客户端向授权服务器发起授权

授权服务器,引导用户进入授权界面,等待用户同意授权

用户同意授权

回调客户端界面,带回授权码

客户端通过授权码向授权服务器请求获取令牌access_token

适用:安全性最高,最常用的流程

隐藏式implicit

授权码模式简化

与授权码模式相似,但是用户同意授权后直接返回access_token

适用:纯前端项目

密码模式password

客户端与资源所有者高度可信情况下可用,如客户端为系统的一部分

资源拥有者直接提供账号密码给客户端,向授权服务器申请令牌access_token

适用:高度信任的情况下

客户端模式client credentials

该情况适用于无前端的纯后台项目,客户端向授权服务器发送身份信息,请求access_token

适用:纯后端项目

OAuth2简单运用实例(SpringBoot简单整合)

step1基础配置

本例资源服务,认证服务均为同一个服务,采用客户端模式(client_credentials)

1.yml文件

  1. server:
  2. port: 8888
  3. spring:
  4. application:
  5. name: oauth2-demo

2.pom文件依赖

引入安全认证依赖,引入后,springboot便会对资源服务器上所有资源进行默认的保护

<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

step2认证服务器配置(包括3部分)

1.token endpoint安全束缚配置

主要配置允许客户端以Form表单形式登录/配置密码加密方式等)

2.客户端详情设置

客户端详情包括(client_id,client_secret,grant_type,scope)

剋设置客户端详情存储位置,内存或者数据库

3.配置授权,token endpoint,令牌服务

认证服务简单配置

本例采用客户端模式

  1. @Configuration
  2. @EnableAuthorizationServer
  3. public class MyAuthorizationConfig extends AuthorizationServerConfigurerAdapter {
  4. //配置采用的加密方式
  5. @Bean
  6. public PasswordEncoder passwordEncoder(){
  7. return new BCryptPasswordEncoder();
  8. }
  9. //配置安全约束 定义令牌终结点上的安全约束
  10. @Override
  11. public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
  12. //允许客户端以form表单登录,如微信获取access token
  13. security.allowFormAuthenticationForClients();
  14. }
  15. //配置客户端详细信息
  16. @Override
  17. public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  18. clients.inMemory()
  19. //client_id
  20. .withClient("client")
  21. //授权方式 "authorization_code", "password", "client_credentials", "implicit", "refresh_token"
  22. .authorizedGrantTypes("client_credentials")
  23. // 授权范围 all表示所有,write等
  24. .scopes("all")
  25. // client_secret配置加密类型,如果不需要 则直接 .secret("{noop}123456"); {noop表示空操作}
  26. .secret(new BCryptPasswordEncoder().encode("123456"));
  27. }
  28. //令牌访问端点配置 可以完成令牌服务以及令牌服务各个endpoint配置
  29. @Override
  30. public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
  31. super.configure(endpoints);
  32. }
  33. }

step3资源服务配置(2个部分)

1.资源服务器安全配置

2.http安全配置,保护资源API

配置类

  1. @Configuration
  2. @EnableResourceServer
  3. public class MyResourceServerConfigurer extends ResourceServerConfigurerAdapter {
  4. @Override
  5. public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
  6. super.configure(resources);
  7. }
  8. @Override
  9. public void configure(HttpSecurity http) throws Exception {
  10. http.authorizeRequests()
  11. .antMatchers("/apple/**").authenticated();
  12. }
  13. }

资源请求API

  1. @RestController
  2. @RequestMapping("/apple")
  3. public class AppleController {
  4. @GetMapping
  5. public String getApple(){
  6. return "资源1苹果";
  7. }
  8. }

至此简单的认证服务与资源服务配置完成,本例认证服务与资源服务为同一个项目

未授权进行资源的请求,此时报错,提示未授权

 获取token

SpringBoot OAuth 默认获取token的endpoint路劲为 /oauth/token

 其中expires_in 为失效时间(秒),每次重复请求,返回同一个token,但是失效时间再减少

 带上token进行请求,得到结果

客户端模式整合数据库

目的:将客户端Client信息与token存储至数据库(token一般存放redis)

本例中用到postgre sql

简单创建需要的表

  1. create table oauth_client_details (
  2. client_id VARCHAR(256) PRIMARY KEY, -- 客户端id
  3. resource_ids VARCHAR(256) , -- 资源id集合,英文逗号隔开
  4. client_secret VARCHAR(256) , -- 客户端密钥
  5. scope VARCHAR(256) , -- 授权范围
  6. authorized_grant_types VARCHAR(256) , -- 授权类型 authorization_code,password,refresh_token,implicit,client_credentials,英文逗号隔开
  7. web_server_redirect_uri VARCHAR(256), -- 客户端重定向uri
  8. authorities VARCHAR(256), -- 客户端拥有spring security权限值
  9. access_token_validity INTEGER , -- token有效时间 秒 默认12小时
  10. refresh_token_validity INTEGER , -- refresh_token有效时间 默认30天
  11. additional_information VARCHAR(4096) , -- 预留json字段
  12. autoapprove VARCHAR(256) -- 是否启动自动approve操作,适用于授权码模式authorization_code
  13. );
  14. create table oauth_access_token (
  15. token_id VARCHAR(256) , -- MD5算法加密后的access_token
  16. token bytea, -- access_token序列化二进制数据格式 mysql中为BLOB类型
  17. authentication_id VARCHAR(256) PRIMARY KEY , -- 根据username,client_id,scopeMD5加密生成的主键
  18. user_name VARCHAR(256), -- 用户名
  19. client_id VARCHAR(256), -- 客户端id
  20. authentication bytea, -- OAuth2Authentication对象序列化后的二进制数
  21. refresh_token VARCHAR(256) -- refresh_token MD5加密后的值
  22. );

官方完整建表sql

https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql

创建客户端数据

注意此处密码应该进行BCrypt加密

123456 对应

$2a$10$FB98FkLGqA2sBEURWp4un.sW2VCSKd7NjT2o2GK/4njggHkEeYaZu

INSERT INTO oauth_client_details (client_id, resource_ids, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) VALUES ('client', 'apple', '$2a$10$FB98FkLGqA2sBEURWp4un.sW2VCSKd7NjT2o2GK/4njggHkEeYaZu', 'all', 'client_credentials', null, null, null, null, null, null);

pom文件新增对应数据库的支持依赖

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>

yml

  1. server:
  2. port: 8888
  3. spring:
  4. application:
  5. name: oauth2-demo
  6. datasource:
  7. # 数据库引擎
  8. driver-class-name: org.postgresql.Driver
  9. # 数据库地址 characterEncoding防止出现中文乱码 若为https则无需加useSSL
  10. url: jdbc:postgresql://127.0.0.1:5432/test?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&yearIsDateType=false&stringtype=unspecified
  11. # 用户
  12. username: postgres
  13. # 密码
  14. password: 123456
  15. # 数据库连接类型
  16. # 时区
  17. jackson:
  18. time-zone: GMT+8

资源服务修改

给资源配置resourceId

  1. @Override
  2. public void configure(ResourceServerSecurityConfigurer resources) throws Exception
  3. //super.configure(resources);
  4. resources.resourceId("apple");
  5. }

认证服务配置修改

从原来从内存中获取客户端信息改为从数据库获得客户端信息

  1. @Configuration
  2. @EnableAuthorizationServer
  3. public class MyAuthorizationConfig extends AuthorizationServerConfigurerAdapter {
  4. private final DataSource dataSource;
  5. public MyAuthorizationConfig(DataSource dataSource) {
  6. this.dataSource = dataSource;
  7. }
  8. //配置采用的加密方式
  9. @Bean
  10. public PasswordEncoder passwordEncoder(){
  11. return new BCryptPasswordEncoder();
  12. }
  13. @Override
  14. public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  15. clients.jdbc(dataSource);
  16. }
  17. //配置安全约束 定义令牌终结点上的安全约束
  18. @Override
  19. public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
  20. //允许客户端以form表单登录,如微信获取access token
  21. security.allowFormAuthenticationForClients();
  22. }
  23. //令牌访问端点配置 可以完成令牌服务以及令牌服务各个endpoint配置
  24. @Override
  25. public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
  26. endpoints.tokenStore(new JdbcTokenStore(dataSource));
  27. }
  28. }

请求测试

每次请求,OAuth会去客户端信息表oauth_client_details查询当前发送过来的客户信息是否正确,认证成功后将返回令牌access_token,同时由于设置了将令牌存储至数据库,此时oauth_access_token也会新增一条数据

postman认证成功返回令牌 

 

成功获取资源服务的资源

 至此OAuth2 客户端授权模式结合数据库存储的简单例子结束,本例中创建了两张需要的表,一为客户端详情表,代替之前在代码中将客户端信息写入内存的方式,二是令牌token表,用于存储token,但实际开发中,token一般不存储在数据库中,不然每次请求接口都需要访问数据库

access_token的存储方案

上面的例子已经设计将token存在内存,数据库中,但更合理的应该存在redis上,且redis可设置数据的有效期。这样可以避免高并发情况下频繁访问数据库。在分布式架构中,将token存在其中一台服务器实例的内存中也不合适。

将token存储至redis

pom新增redis依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. </dependency>

yml配置文件新增redis配置

redis:
  # Redis数据库索引(默认为0)
  database: 0
  # Redis服务器地址 本地localhost/127.0.0.1
  host: 127.0.0.1
  # Redis服务器连接端口
  port: 6379
  # 连接超时时长(毫秒)
  timeout: 6000
  # Redis服务器连接密码(默认为空)
  password:

认证服务MyAuthorizationConfig修改

令牌访问端点修改为redis存储方式

  1. @Resource
  2. private RedisConnectionFactory redisConnectionFactory;
  3. ......
  4. //令牌访问端点配置 可以完成令牌服务以及令牌服务各个endpoint配置
  5. @Override
  6. public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
  7. endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory));
  8. //endpoints.tokenStore(new JdbcTokenStore(dataSource));
  9. }

启动本地redis,启动服务请求获得token,此时redis中

通过JWT充当令牌

JWT简单介绍

JSON Web Token(JWT),客户端服务器通过JWT规定格式进行身份认证完成交互,JWT分三段,每段通过.隔开,包含不同信息分别为

Header头部:JSON数据Base64编码,包含加密类型等

PayLoad负载:JSON数据Base64编码,包含客户端授权信息

Signature签名:头部+负载+密钥(盐)通过加密算法获得,防止数据篡改(盐存在服务端,保密)

导入依赖

  1. <dependency>
  2. <groupId>org.springframework.security</groupId>
  3. <artifactId>spring-security-jwt</artifactId>
  4. <version>1.1.1.RELEASE</version>
  5. </dependency>

修改认证服务配置

  1. //设置密钥串
  2. private static final String SIGNING_KEY = "oauth_test";
  3. ......
  4. //令牌访问端点配置 可以完成令牌服务以及令牌服务各个endpoint配置
  5. @Override
  6. public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{
  7. //设置token转换器
  8. JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
  9. //设置加签密钥
  10. jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
  11. //设置校验器
  12. jwtAccessTokenConverter.setVerifier(new MacSigner(SIGNING_KEY));
  13. TokenStore tokenStore = new JwtTokenStore(jwtAccessTokenConverter);
  14. endpoints.accessTokenConverter(jwtAccessTokenConverter);
  15. endpoints.tokenStore(tokenStore);
  16. }

 启动服务请求获得token,并成功请求资源接口

 此token通过官网解码JSON Web Tokens - jwt.ioJSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed using JSON Web Signature (JWS).https://jwt.io/

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/84343
推荐阅读
相关标签
  

闽ICP备14008679号