Spring Security 最佳实践,看了必懂!

打印 上一主题 下一主题

主题 841|帖子 841|积分 2523

作者:清茶淡粥酱
链接:https://juejin.cn/post/7026734817853210661
Spring Security简介

Spring Security 是一种高度自定义的安全框架,利用(基于)SpringIOC/DI和AOP功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作
核心功能:认证和授权
Spring Security 认证流程


Spring Security 项目搭建

导入依赖

Spring Security已经被Spring boot进行集成,使用时直接引入启动器即可
  1. <dependency>
  2.     <groupId>org.springframework.boot</groupId>
  3.     <artifactId>spring-boot-starter-security</artifactId>
  4. </dependency>
复制代码
Spring Boot 基础就不介绍了,推荐下这个实战教程:
https://github.com/javastacks/spring-boot-best-practice
访问页面

导入spring-boot-starter-security启动器后,Spring Security已经生效,默认拦截全部请求,如果用户没有登录,跳转到内置登录页面。
在浏览器输入:http://localhost:8080/ 进入Spring Security内置登录页面
用户名: user
密码:项目启动,打印在控制台中
自定义用户名和密码

修改application.yml 文件
  1. # 静态用户,一般只在内部网络认证中使用,如:内部服务器1,访问服务器2
  2. spring:
  3.   security:
  4.     user:
  5.       name: test  # 通过配置文件,设置静态用户名
  6.       password: test # 配置文件,设置静态登录密码
复制代码
UserDetailsService详解

什么也没有配置的时候,账号和密码是由Spring Security定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。如果需要自定义逻辑时,只需要实现UserDetailsService接口
  1. @Component
  2. public class UserSecurity implements UserDetailsService {
  3.     @Autowired
  4.     private UserService userService;
  5.     @Override
  6.     public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
  7.         User user = userService.login(userName);
  8.         System.out.println(user);
  9.         if (null==user){
  10.             throw new UsernameNotFoundException("用户名错误");
  11.         }
  12.         org.springframework.security.core.userdetails.User result =
  13.                 new org.springframework.security.core.userdetails.User(
  14.                         userName,user.getPassword(), AuthorityUtils.createAuthorityList()
  15.                 );
  16.         return result;
  17.     }
  18. }
复制代码
推荐一个 Spring Boot 基础教程:
https://github.com/javastacks/spring-boot-best-practice
PasswordEncoder密码解析器详解

PasswordEncoder

PasswordEncoder 是SpringSecurity  的密码解析器,用户密码校验、加密 。 自定义登录逻辑时要求必须给容器注入PaswordEncoder的bean对象
SpringSecurity 定义了很多实现接口PasswordEncoder  满足我们密码加密、密码校验 使用需求
自定义密码解析器


  • 编写类,实现PasswordEncoder 接口
  1. /**
  2. * 凭证匹配器,用于做认证流程的凭证校验使用的类型
  3. * 其中有2个核心方法
  4. * 1. encode - 把明文密码,加密成密文密码
  5. * 2. matches - 校验明文和密文是否匹配
  6. * */
  7. public class MyMD5PasswordEncoder implements PasswordEncoder {
  8.     /**
  9.      * 加密
  10.      * @param charSequence  明文字符串
  11.      * @return
  12.      */
  13.     @Override
  14.     public String encode(CharSequence charSequence) {
  15.         try {
  16.             MessageDigest digest = MessageDigest.getInstance("MD5");
  17.             return toHexString(digest.digest(charSequence.toString().getBytes()));
  18.         } catch (NoSuchAlgorithmException e) {
  19.             e.printStackTrace();
  20.             return "";
  21.         }
  22.     }
  23.     /**
  24.      * 密码校验
  25.      * @param charSequence 明文,页面收集密码
  26.      * @param s 密文 ,数据库中存放密码
  27.      * @return
  28.      */
  29.     @Override
  30.     public boolean matches(CharSequence charSequence, String s) {
  31.         return s.equals(encode(charSequence));
  32.     }
  33.      /**
  34.      * @param tmp 转16进制字节数组
  35.      * @return 饭回16进制字符串
  36.      */
  37.     private String toHexString(byte [] tmp){
  38.         StringBuilder builder = new StringBuilder();
  39.         for (byte b :tmp){
  40.             String s = Integer.toHexString(b & 0xFF);
  41.             if (s.length()==1){
  42.                 builder.append("0");
  43.             }
  44.             builder.append(s);
  45.         }
  46.         return builder.toString();
  47.     }
  48. }
复制代码
2.在配置类中指定自定义密码凭证匹配器
  1. /**
  2.   * 加密
  3.   * @return 加密对象
  4.   * 如需使用自定义密码凭证匹配器 返回自定义加密对象
  5.   * 例如: return new MD5PasswordEncoder();
  6.   */
  7. @Bean
  8. public PasswordEncoder passwordEncoder() {
  9.     return new BCryptPasswordEncoder(); //Spring Security 自带
  10. }
复制代码
登录配置

方式一 转发
  1. http.formLogin()
  2.     .usernameParameter("name") // 设置请求参数中,用户名参数名称。 默认username
  3.     .passwordParameter("pswd") // 设置请求参数中,密码参数名称。 默认password
  4.     .loginPage("/toLogin") // 当用户未登录的时候,跳转的登录页面地址是什么? 默认 /login
  5.     .loginProcessingUrl("/login") // 用户登录逻辑请求地址是什么。 默认是 /login
  6.     .failureForwardUrl("/failure"); // 登录失败后,请求转发的位置。Security请求转发使用Post请求。默认转发到: loginPage?error
  7.     .successForwardUrl("/toMain"); // 用户登录成功后,请求转发到的位置。Security请求转发使用POST请求。
复制代码
方式二 :重定向
  1. http.formLogin()
  2.     .usernameParameter("name") // 设置请求参数中,用户名参数名称。 默认username
  3.     .passwordParameter("pswd") // 设置请求参数中,密码参数名称。 默认password
  4.     .loginPage("/toLogin") // 当用户未登录的时候,跳转的登录页面地址是什么? 默认 /login
  5.     .loginProcessingUrl("/login") // 用户登录逻辑请求地址是什么。 默认是 /login
  6.         .defaultSuccessUrl("/toMain",true); //用户登录成功后,响应重定向到的位置。 GET请求。必须配置绝对地址。
  7.          .failureUrl("/failure"); // 登录失败后,重定向的位置。
复制代码
方式三:自定义登录处理器

自定义登录失败逻辑处理器
  1. /*自定义登录失败处理器*/
  2. public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
  3.     private  String url;
  4.     private boolean isRedirect;
  5.     public MyAuthenticationFailureHandler(String url, boolean isRedirect) {
  6.         this.url = url;
  7.         this.isRedirect = isRedirect;
  8.     }
  9.     @Override
  10.     public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
  11.         if (isRedirect){
  12.             httpServletResponse.sendRedirect(url);
  13.         }else {
  14.             httpServletRequest.getRequestDispatcher(url).forward(httpServletRequest,httpServletResponse);
  15.         }
  16.     }
  17. //get set 方法 省略
复制代码
自定义登录成功逻辑处理器
  1. /**
  2. * 自定义登录成功后处理器
  3. * 转发重定向,有代码逻辑实现
  4. * */
  5. public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
  6.     private String url;
  7.     private boolean isRedirect;
  8.     public MyAuthenticationSuccessHandler(String url, boolean isRedirect) {
  9.         this.url = url;
  10.         this.isRedirect = isRedirect;
  11.     }
  12.     /**
  13.      * @param request 请求对象 request.getRequestDispatcher.forward()
  14.      * @param response 响应对象 response.sendRedirect()
  15.      * @param authentication 用户认证成功后的对象。其中报换用户名权限结合,内容是
  16.      *                       自定义UserDetailsService
  17.      * */
  18.     @Override
  19.     public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
  20.         if (isRedirect){
  21.             response.sendRedirect(url);
  22.         }else {
  23.             request.getRequestDispatcher(url).forward(request,response);
  24.         }
  25.     }
  26. //get set 方法 省略   
  27. http.formLogin()
  28.     .usernameParameter("name") // 设置请求参数中,用户名参数名称。 默认username
  29.     .passwordParameter("pswd") // 设置请求参数中,密码参数名称。 默认password
  30.     .loginPage("/toLogin") // 当用户未登录的时候,跳转的登录页面地址是什么? 默认 /login
  31.     .loginProcessingUrl("/login") // 用户登录逻辑请求地址是什么。 默认是 /login
复制代码
登录相关配置类
  1. @Configuration
  2. @EnableWebSecurity
  3. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  4.     @Autowired
  5.     private  UserSecurity userSecurity;
  6.     @Autowired
  7.     private PersistentTokenRepository persistentTokenRepository;
  8.     /**
  9.      * 加密
  10.      * @return 加密对象
  11.      * 如需使用自定义加密逻辑 返回自定义加密对象
  12.      * return new MD5PasswordEncoder(); return new SimplePasswordEncoder();
  13.      */
  14.     @Bean
  15.     public PasswordEncoder passwordEncoder() {
  16.         return new BCryptPasswordEncoder(); //Spring Security 自带
  17.     }
  18.     @Override
  19.     protected void configure(HttpSecurity http) throws Exception {
  20.         // 配置登录请求相关内容。
  21.         http.formLogin()
  22.             .loginPage("/toLogin") // 当用户未登录的时候,跳转的登录页面地址是什么? 默认 /login
  23.             .usernameParameter("name") // 设置请求参数中,用户名参数名称。 默认username
  24.             .passwordParameter("pswd") // 设置请求参数中,密码参数名称。 默认password
  25.             .loginProcessingUrl("/login") //设置登录 提交表单数据访问请求地址
  26.             .defaultSuccessUrl("/toMain")   
  27.             .failureUrl("/toLogin");
  28.                 //.successForwardUrl("/toMain")
  29.                        //.failureForwardUrl("/toLogin");
  30.             //.successHandler(new LoginSuccessHandler("/toMain", true)) //自定义登录成功处理器
  31.                 //.failureHandler(new LoginErrorHandler("/toLogin", true));
  32.         http.authorizeRequests()
  33.             //.antMatchers("/toLogin").anonymous() //只能匿名用户访问
  34.             .antMatchers("/toLogin", "/register", "/login", "/favicon.ico").permitAll() // /toLogin请求地址,可以随便访问。
  35.             .antMatchers("/**/*.js").permitAll() // 授予所有目录下的所有.js文件可访问权限
  36.             .regexMatchers(".*[.]css").permitAll() // 授予所有目录下的所有.css文件可访问权限
  37.             .anyRequest().authenticated(); // 任意的请求,都必须认证后才能访问。
  38.         // 配置退出登录
  39.         http.logout()
  40.                 .invalidateHttpSession(true) // 回收HttpSession对象。退出之前调用HttpSession.invalidate() 默认 true
  41.                 .clearAuthentication(true) // 退出之前,清空Security记录的用户登录标记。 默认 true
  42.                 // .addLogoutHandler() // 增加退出处理器。
  43.                 .logoutSuccessUrl("/") // 配置退出后,进入的请求地址。 默认是loginPage?logout
  44.                 .logoutUrl("/logout"); // 配置退出登录的路径地址。和页面请求地址一致即可。
  45.         // 关闭CSRF安全协议。
  46.         // 关闭是为了保证完整流程的可用。
  47.         http.csrf().disable();
  48.     }
  49.    @Bean
  50.    public PersistentTokenRepository persistentTokenRepository(DataSource dataSource){
  51.         JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
  52.         jdbcTokenRepository.setDataSource(dataSource);
  53.         //jdbcTokenRepository.setCreateTableOnStartup(true);
  54.         return jdbcTokenRepository;
  55.     }
  56. }
复制代码
角色权限

hasAuthority(String)   判断角色是否具有特定权限
  1. http.authorizeRequests().antMatchers("/main1.html").hasAuthority("admin")
复制代码
hasAnyAuthority(String ...)  如果用户具备给定权限中某一个,就允许访问
  1. http.authorizeRequests().antMatchers("/admin/read").hasAnyAuthority("xxx","xxx")
复制代码
hasRole(String)  如果用户具备给定角色就允许访问。否则出现403
  1. //请求地址为/admin/read的请求,必须登录用户拥有'管理员'角色才可访问
  2. http.authorizeRequests().antMatchers("/admin/read").hasRole("管理员")
复制代码
hasAnyRole(String ...) 如果用户具备给定角色的任意一个,就允许被访问
  1. //用户拥有角色是管理员 或 访客 可以访问 /guest/read
  2. http.authorizeRequests().antMatchers("/guest/read").hasAnyRole("管理员", "访客")
复制代码
hasIpAddress(String) 请求是指定的IP就运行访问
  1. //ip 是127.0.0.1 的请求 可以访问/ip
  2. http.authorizeRequests().antMatchers("/ip").hasIpAddress("127.0.0.1")
复制代码
403 权限不足页面处理

1.编写类实现接口AccessDeniedHandler
  1. /**
  2. * @describe  403 权限不足
  3. * @author: AnyWhere
  4. * @date 2021/4/18 20:57
  5. */
  6. @Component
  7. public class MyAccessDeniedHandler implements AccessDeniedHandler {
  8.     @Override
  9.     public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)
  10.             throws IOException, ServletException {
  11.         response.setStatus(HttpServletResponse.SC_OK);
  12.         response.setContentType("text/html;charset=UTF-8");
  13.         response.getWriter().write(
  14.                 "<html>" +
  15.                         "<body>" +
  16.                         "" +
  17.                         "权限不足,请联系管理员" +
  18.                         "" +
  19.                         "</body>" +
  20.                         "</html>"
  21.         );
  22.         response.getWriter().flush();//刷新缓冲区
  23.     }
  24. }
复制代码
2.配置类中配置exceptionHandling
  1. // 配置403访问错误处理器。
  2. http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);/
复制代码
RememberMe(记住我)
  1. @Configuration
  2. @EnableWebSecurity
  3. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  4.   @Override
  5.   protected void configure(HttpSecurity http) throws Exception {
  6.     //配置记住密码
  7.     http.rememberMe()
  8.         .rememberMeParameter("remember-me") // 修改请求参数名。 默认是remember-me
  9.         .tokenValiditySeconds(14*24*60*60) // 设置记住我有效时间。单位是秒。默认是14天
  10.         .rememberMeCookieName("remember-me") // 修改remember me的cookie名称。默认是remember-me
  11.         .tokenRepository(persistentTokenRepository) // 配置用户登录标记的持久化工具对象。
  12.         .userDetailsService(userSecurity); // 配置自定义的UserDetailsService接口实现类对象
  13.   }
  14.   @Bean
  15.   public PersistentTokenRepository persistentTokenRepository(DataSource dataSource){
  16.      JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
  17.      jdbcTokenRepository.setDataSource(dataSource);
  18.      //jdbcTokenRepository.setCreateTableOnStartup(true);
  19.      return jdbcTokenRepository;
  20.   }
  21. }   
复制代码
Spring Security 注解

@Secured

角色校验 ,请求到来访问控制单元方法时必须包含XX角色才能访问
角色必须添加ROLE_前缀
  1.   @Secured({"ROLE_管理员","ROLE_访客"})
  2.   @RequestMapping("/toMain")
  3.   public String toMain(){
  4.       return "main";
  5.   }
复制代码
使用注解@Secured需要在配置类中添加注解 使@Secured注解生效
  1. @EnableGlobalMethodSecurity(securedEnabled = true)
复制代码
@PreAuthorize

权限检验,请求到来访问控制单元之前必须包含xx权限才能访问,控制单元方法执行前进行角色校验
  1.    /**
  2.      * [ROLE_管理员, admin:read, admin:write, all:login, all:logout, all:error, all:toMain]
  3.      * @PreAuthorize   角色 、权限 校验 方法执行前进行角色校验
  4.      *
  5.      *  hasAnyAuthority()
  6.      *  hasAuthority()
  7.      *
  8.      *  hasPermission()
  9.      *
  10.      *
  11.      *  hasRole()   
  12.      *  hasAnyRole()
  13.      * */
  14.     @PreAuthorize("hasAnyRole('ROLE_管理员','ROLE_访客')")
  15.     @RequestMapping("/toMain")
  16.     @PreAuthorize("hasAuthority('admin:write')")
  17.     public String toMain(){
  18.         return "main";
  19.     }
复制代码
使用@PreAuthorize和@PostAuthorize 需要在配置类中配置注解@EnableGlobalMethodSecurity 才能生效
  1. @EnableGlobalMethodSecurity(prePostEnabled = true)
复制代码
@PostAuthorize

权限检验,请求到来访问控制单元之后必须包含xx权限才能访问 ,控制单元方法执行完后进行角色校验
  1.    /**
  2.      * [ROLE_管理员, admin:read, admin:write, all:login, all:logout, all:error, all:toMain]
  3.      * @PostAuthorize  角色 、权限 校验 方法执行后进行角色校验
  4.      *
  5.      *  hasAnyAuthority()
  6.      *  hasAuthority()
  7.      *  hasPermission()
  8.      *  hasRole()
  9.      *  hasAnyRole()
  10.      * */
  11.     @PostAuthorize("hasRole('ROLE_管理员')")
  12.     @RequestMapping("/toMain")
  13.     @PreAuthorize("hasAuthority('admin:write')")
  14.     public String toMain(){
  15.         return "main";
  16.     }
复制代码
Spring Security 整合Thymeleaf 进行权限校验
  1. <dependency>
  2.       <groupId>org.springframework.boot</groupId>
  3.       <artifactId>spring-boot-starter-thymeleaf</artifactId>
  4. </dependency>
  5. <dependency>
  6.      <groupId>org.thymeleaf.extras</groupId>
  7.      <artifactId>thymeleaf-extras-springsecurity5</artifactId>
  8. </dependency>
复制代码
Spring Security中CSRF

什么是CSRF?

CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack” 或者Session Riding。通过伪造用户请求访问受信任站点的非法请求访问。
跨域:只要网络协议,ip地址,端口中任何一个不相同就是跨域请求。
客户端与服务进行交互时,由于http协议本身是无状态协议,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id可能被第三方恶意劫持,通过这个session id向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情。
通俗解释:
CSRF就是别的网站非法获取我们网站Cookie值,我们项目服务器是无法区分到底是不是我们的客户端,只有请求中有Cookie,认为是自己的客户端,所以这个时候就出现了CSRF。
近期热文推荐:
1.1,000+ 道 Java面试题及答案整理(2022最新版)
2.劲爆!Java 协程要来了。。。
3.Spring Boot 2.x 教程,太全了!
4.别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!
5.《Java开发手册(嵩山版)》最新发布,速速下载!
觉得不错,别忘了随手点赞+转发哦!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

张国伟

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表