查看原文
其他

Spring Security 最佳实践

点击关注👉 Java后端栈 2023-09-18

推荐关注

以下文章来源Java后端栈,回复”面试“获面试宝典


哈喽,各位新来的小伙伴们,大家好!由于公众号做了改版,为了保证公众号的资源能准时推送到你手里,大家记得将后端君的公众号 加星标置顶 ,在此真诚的表示感谢~

编辑:后端妹 | 来源:juejin.cn/post/7026734817853210661

上一篇阿里官方 Redis 开发规范



正文


大家好,我是栈哥。

  • Spring Security简介- Spring Security 认证流程
  • Spring Security 项目搭建
  • UserDetailsService详解
  • PasswordEncoder密码解析器详解
  • 登录配置
  • 角色权限
  • 403 权限不足页面处理
  • RememberMe(记住我)
  • Spring Security 注解
  • Spring Security中CSRF

今天来一篇 Spring Security 精讲,相信你看过之后能彻底搞懂 Spring Security。

Spring Security简介

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

核心功能:认证和授权

Spring Security 认证流程

图片

Spring Security 项目搭建

导入依赖

Spring Security已经被Spring boot进行集成,使用时直接引入启动器即可

<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-security</artifactId>  
</dependency>  

访问页面

导入spring-boot-starter-security启动器后,Spring Security已经生效,默认拦截全部请求,如果用户没有登录,跳转到内置登录页面。

在浏览器输入:http://localhost:8080/ 进入Spring Security内置登录页面

用户名:user。

密码:项目启动,打印在控制台中。

自定义用户名和密码

修改application.yml文件

# 静态用户,一般只在内部网络认证中使用,如:内部服务器1,访问服务器2  
spring:  
  security:  
    user:  
      name: test  # 通过配置文件,设置静态用户名  
      password: test # 配置文件,设置静态登录密码  

UserDetailsService详解

什么也没有配置的时候,账号和密码是由Spring Security定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过 「自定义逻辑控制认证逻辑」  。如果需要自定义逻辑时,只需要实现UserDetailsService接口

@Component  
public class UserSecurity implements UserDetailsService {  
  
    @Autowired  
    private UserService userService;  
  
    @Override  
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {  
  
        User user = userService.login(userName);  
        System.out.println(user);  
        if (null==user){  
            throw new UsernameNotFoundException("用户名错误");  
        }  
        org.springframework.security.core.userdetails.User result =  
                new org.springframework.security.core.userdetails.User(  
                        userName,user.getPassword(), AuthorityUtils.createAuthorityList()  
                );  
        return result;  
    }  
  
}  

PasswordEncoder密码解析器详解

PasswordEncoder

「PasswordEncoder」 是SpringSecurity 的密码解析器,用户密码校验、加密 。自定义登录逻辑时要求必须给容器注入PaswordEncoder的bean对象

SpringSecurity 定义了很多实现接口 「PasswordEncoder」 满足我们密码加密、密码校验 使用需求。另外,搜索公众号顶级科技后台回复“API接口”,获取一份惊喜礼包。

PasswordEncoder密码解析器详解

自定义密码解析器

  1. 编写类,实现PasswordEncoder 接口
/**  
 * 凭证匹配器,用于做认证流程的凭证校验使用的类型  
 * 其中有2个核心方法  
 * 1. encode - 把明文密码,加密成密文密码  
 * 2. matches - 校验明文和密文是否匹配  
 * */
  
public class MyMD5PasswordEncoder implements PasswordEncoder {  
  
    /**  
     * 加密  
     * @param charSequence  明文字符串  
     * @return  
     */
  
    @Override  
    public String encode(CharSequence charSequence) {  
        try {  
            MessageDigest digest = MessageDigest.getInstance("MD5");  
            return toHexString(digest.digest(charSequence.toString().getBytes()));  
        } catch (NoSuchAlgorithmException e) {  
            e.printStackTrace();  
            return "";  
        }  
    }  
  
    /**  
     * 密码校验  
     * @param charSequence 明文,页面收集密码  
     * @param s 密文 ,数据库中存放密码  
     * @return  
     */
  
    @Override  
    public boolean matches(CharSequence charSequence, String s) {  
        return s.equals(encode(charSequence));  
    }  
  
     /**  
     * @param tmp 转16进制字节数组  
     * @return 饭回16进制字符串  
     */
  
    private String toHexString(byte [] tmp){  
        StringBuilder builder = new StringBuilder();  
        for (byte b :tmp){  
            String s = Integer.toHexString(b & 0xFF);  
            if (s.length()==1){  
                builder.append("0");  
            }  
            builder.append(s);  
        }  
  
        return builder.toString();  
  
    }  
}  
  1. 在配置类中指定自定义密码凭证匹配器。扩展:快速开发平台
/**  
  * 加密  
  * @return 加密对象  
  * 如需使用自定义密码凭证匹配器 返回自定义加密对象  
  * 例如: return new MD5PasswordEncoder();   
  */
  
@Bean  
public PasswordEncoder passwordEncoder() {  
    return new BCryptPasswordEncoder(); //Spring Security 自带  
}  

登录配置

方式一 转发

http.formLogin()  
    .usernameParameter("name") // 设置请求参数中,用户名参数名称。 默认username  
    .passwordParameter("pswd") // 设置请求参数中,密码参数名称。 默认password  
    .loginPage("/toLogin") // 当用户未登录的时候,跳转的登录页面地址是什么? 默认 /login  
    .loginProcessingUrl("/login") // 用户登录逻辑请求地址是什么。 默认是 /login  
    .failureForwardUrl("/failure"); // 登录失败后,请求转发的位置。Security请求转发使用Post请求。默认转发到:loginPage?error  
    .successForwardUrl("/toMain"); // 用户登录成功后,请求转发到的位置。Security请求转发使用POST请求。  

方式二 :重定向

http.formLogin()  
    .usernameParameter("name") // 设置请求参数中,用户名参数名称。 默认username  
    .passwordParameter("pswd") // 设置请求参数中,密码参数名称。 默认password  
    .loginPage("/toLogin") // 当用户未登录的时候,跳转的登录页面地址是什么? 默认 /login  
    .loginProcessingUrl("/login") // 用户登录逻辑请求地址是什么。 默认是 /login  
 .defaultSuccessUrl("/toMain",true); //用户登录成功后,响应重定向到的位置。GET请求。必须配置绝对地址。  
  .failureUrl("/failure"); // 登录失败后,重定向的位置。  

方式三:自定义登录处理器

自定义登录失败逻辑处理器

/*自定义登录失败处理器*/  
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {  
    private  String url;  
    private boolean isRedirect;  
  
  
    public MyAuthenticationFailureHandler(String url, boolean isRedirect) {  
        this.url = url;  
        this.isRedirect = isRedirect;  
    }  
  
    @Override  
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {  
        if (isRedirect){  
            httpServletResponse.sendRedirect(url);  
        }else {  
            httpServletRequest.getRequestDispatcher(url).forward(httpServletRequest,httpServletResponse);  
        }  
    }  
  
//get set 方法 省略  

自定义登录成功逻辑处理器

/**  
 * 自定义登录成功后处理器  
 * 转发重定向,有代码逻辑实现  
 * */
  
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {  
    private String url;  
    private boolean isRedirect;  
  
    public MyAuthenticationSuccessHandler(String url, boolean isRedirect) {  
        this.url = url;  
        this.isRedirect = isRedirect;  
    }  
  
    /**  
     * @param request 请求对象 request.getRequestDispatcher.forward()  
     * @param response 响应对象 response.sendRedirect()  
     * @param authentication 用户认证成功后的对象。其中报换用户名权限结合,内容是  
     *                       自定义UserDetailsService  
     * */
  
    @Override  
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {  
        if (isRedirect){  
            response.sendRedirect(url);  
        }else {  
            request.getRequestDispatcher(url).forward(request,response);  
        }  
    }  
  
//get set 方法 省略     
http.formLogin()  
    .usernameParameter("name"// 设置请求参数中,用户名参数名称。 默认username  
    .passwordParameter("pswd"// 设置请求参数中,密码参数名称。 默认password  
    .loginPage("/toLogin"// 当用户未登录的时候,跳转的登录页面地址是什么? 默认 /login  
    .loginProcessingUrl("/login"// 用户登录逻辑请求地址是什么。 默认是 /login  

登录相关配置类

牛逼啊!接私活必备的 N 个开源项目!赶快收藏

@Configuration  
@EnableWebSecurity  
public class SecurityConfig extends WebSecurityConfigurerAdapter {  
  
    @Autowired  
    private  UserSecurity userSecurity;  
    @Autowired  
    private PersistentTokenRepository persistentTokenRepository;  
  
  
    /**  
     * 加密  
     * @return 加密对象  
     * 如需使用自定义加密逻辑 返回自定义加密对象  
     * return new MD5PasswordEncoder(); return new SimplePasswordEncoder();  
     */
  
    @Bean  
    public PasswordEncoder passwordEncoder() {  
        return new BCryptPasswordEncoder(); //Spring Security 自带  
    }  
  
    @Override  
    protected void configure(HttpSecurity http) throws Exception {  
        // 配置登录请求相关内容。  
        http.formLogin()  
            .loginPage("/toLogin"// 当用户未登录的时候,跳转的登录页面地址是什么? 默认 /login  
            .usernameParameter("name"// 设置请求参数中,用户名参数名称。 默认username  
            .passwordParameter("pswd"// 设置请求参数中,密码参数名称。 默认password  
            .loginProcessingUrl("/login"//设置登录 提交表单数据访问请求地址  
            .defaultSuccessUrl("/toMain")     
            .failureUrl("/toLogin");  
         //.successForwardUrl("/toMain")  
         //.failureForwardUrl("/toLogin");  
            //.successHandler(new LoginSuccessHandler("/toMain", true)) //自定义登录成功处理器  
                //.failureHandler(new LoginErrorHandler("/toLogin", true));  
  
        http.authorizeRequests()  
            //.antMatchers("/toLogin").anonymous() //只能匿名用户访问  
            .antMatchers("/toLogin""/register""/login""/favicon.ico").permitAll() // /toLogin请求地址,可以随便访问。  
            .antMatchers("/**/*.js").permitAll() // 授予所有目录下的所有.js文件可访问权限  
            .regexMatchers(".*[.]css").permitAll() // 授予所有目录下的所有.css文件可访问权限  
            .anyRequest().authenticated(); // 任意的请求,都必须认证后才能访问。  
  
  
        // 配置退出登录  
        http.logout()  
                .invalidateHttpSession(true// 回收HttpSession对象。退出之前调用HttpSession.invalidate() 默认 true  
                .clearAuthentication(true// 退出之前,清空Security记录的用户登录标记。 默认 true  
                // .addLogoutHandler() // 增加退出处理器。  
                .logoutSuccessUrl("/"// 配置退出后,进入的请求地址。 默认是loginPage?logout  
                .logoutUrl("/logout"); // 配置退出登录的路径地址。和页面请求地址一致即可。  
  
        // 关闭CSRF安全协议。  
        // 关闭是为了保证完整流程的可用。  
        http.csrf().disable();  
    }  
  
  
   @Bean  
   public PersistentTokenRepository persistentTokenRepository(DataSource dataSource){  
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();  
        jdbcTokenRepository.setDataSource(dataSource);  
        //jdbcTokenRepository.setCreateTableOnStartup(true);  
        return jdbcTokenRepository;  
    }  
}  

角色权限

「hasAuthority(String)」 判断角色是否具有特定权限

http.authorizeRequests().antMatchers("/main1.html").hasAuthority("admin")  

「hasAnyAuthority(String ...)」 如果用户具备给定权限中某一个,就允许访问

http.authorizeRequests().antMatchers("/admin/read").hasAnyAuthority("xxx","xxx")   

「hasRole(String)」 如果用户具备给定角色就允许访问。否则出现403

//请求地址为/admin/read的请求,必须登录用户拥有'管理员'角色才可访问  
http.authorizeRequests().antMatchers("/admin/read").hasRole("管理员")   

「hasAnyRole(String ...)」 如果用户具备给定角色的任意一个,就允许被访问

//用户拥有角色是管理员 或 访客 可以访问 /guest/read  
http.authorizeRequests().antMatchers("/guest/read").hasAnyRole("管理员""访客")  

「hasIpAddress(String)」 请求是指定的IP就运行访问

//ip 是127.0.0.1 的请求 可以访问/ip  
http.authorizeRequests().antMatchers("/ip").hasIpAddress("127.0.0.1")  

403 权限不足页面处理

  1. 编写类实现接口 「AccessDeniedHandler」
/**  
 * @describe  403 权限不足  
 * @author: AnyWhere   
 */
  
@Component  
public class MyAccessDeniedHandler implements AccessDeniedHandler {  
    @Override  
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)   
            throws IOException, ServletException 
{  
  
        response.setStatus(HttpServletResponse.SC_OK);  
  
        response.setContentType("text/html;charset=UTF-8");  
  
        response.getWriter().write(  
                "<html>" +  
                        "<body>" +  
                        "<div style='width:800px;text-align:center;margin:auto;font-size:24px'>" +  
                        "权限不足,请联系管理员" +  
                        "</div>" +  
                        "</body>" +  
                        "</html>"  
  
        );  
  
        response.getWriter().flush();//刷新缓冲区  
    }  
}  
  1. 配置类中配置exceptionHandling
// 配置403访问错误处理器。  
http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);/  

RememberMe(记住我)

@Configuration  
@EnableWebSecurity  
public class SecurityConfig extends WebSecurityConfigurerAdapter {  
  @Override  
  protected void configure(HttpSecurity http) throws Exception {  
    //配置记住密码  
    http.rememberMe()  
        .rememberMeParameter("remember-me"// 修改请求参数名。 默认是remember-me  
        .tokenValiditySeconds(14*24*60*60// 设置记住我有效时间。单位是秒。默认是14天  
        .rememberMeCookieName("remember-me"// 修改remember me的cookie名称。默认是remember-me  
        .tokenRepository(persistentTokenRepository) // 配置用户登录标记的持久化工具对象。  
        .userDetailsService(userSecurity); // 配置自定义的UserDetailsService接口实现类对象  
  
  }  
  @Bean  
  public PersistentTokenRepository persistentTokenRepository(DataSource dataSource){  
     JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();  
     jdbcTokenRepository.setDataSource(dataSource);  
     //jdbcTokenRepository.setCreateTableOnStartup(true);  
     return jdbcTokenRepository;  
  }  
}     

Spring Security 注解

@Secured

角色校验 ,请求到来访问控制单元方法时必须包含XX角色才能访问

角色必须添加ROLE_前缀

@Secured({"ROLE_管理员","ROLE_访客"})  
@RequestMapping("/toMain")  
public String toMain(){  
    return "main";  
}  

使用注解@Secured需要在配置类中添加注解 使@Secured注解生效

@EnableGlobalMethodSecurity(securedEnabled = true)  

@PreAuthorize

权限检验,请求到来访问控制单元之前必须包含xx权限才能访问,控制单元方法执行前进行角色校验

/**  
 * [ROLE_管理员, admin:read, admin:write, all:login, all:logout, all:error, all:toMain]  
 * @PreAuthorize   角色 、权限 校验 方法执行前进行角色校验  
 *  
 *  hasAnyAuthority()   
 *  hasAuthority()  
 *  
 *  hasPermission()  
 *  
 *  
 *  hasRole()     
 *  hasAnyRole()  
 * */
  

@PreAuthorize("hasAnyRole('ROLE_管理员','ROLE_访客')")  
@RequestMapping("/toMain")  
@PreAuthorize("hasAuthority('admin:write')")  
public String toMain(){  
    return "main";  
}  

使用@PreAuthorize@PostAuthorize 需要在配置类中配置注解@EnableGlobalMethodSecurity 才能生效

@EnableGlobalMethodSecurity(prePostEnabled = true)  

@PostAuthorize

权限检验,请求到来访问控制单元之后必须包含xx权限才能访问 ,控制单元方法执行完后进行角色校验

/**  
 * [ROLE_管理员, admin:read, admin:write, all:login, all:logout, all:error, all:toMain]  
 * @PostAuthorize  角色 、权限 校验 方法执行后进行角色校验  
 *  
 *  hasAnyAuthority()  
 *  hasAuthority()  
 *  hasPermission()  
 *  hasRole()  
 *  hasAnyRole()  
 * */
  
@PostAuthorize("hasRole('ROLE_管理员')")  
@RequestMapping("/toMain")  
@PreAuthorize("hasAuthority('admin:write')")  
public String toMain(){  
    return "main";  
}  

Spring Security 整合Thymeleaf 进行权限校验

<dependency>  
      <groupId>org.springframework.boot</groupId>  
      <artifactId>spring-boot-starter-thymeleaf</artifactId>  
</dependency>  
  
<dependency>  
     <groupId>org.thymeleaf.extras</groupId>  
     <artifactId>thymeleaf-extras-springsecurity5</artifactId>  
</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。


欢迎有需要的同学试试,如果本文对您有帮助,也请帮忙点个 赞 + 在看 啦!❤️ 

在 GitHub猿 还有更多优质项目系统学习资源,欢迎分享给其他同学吧!


最后给读者整理了一份BAT大厂面试真题,需要的可扫码加微信备注:“面试”获取。


◆  ◆  ◆  ◆  ◆ 

(放到你圈子里,朋友们会感激您)PS:如果觉得我的分享不错,欢迎大家随手点赞、在看。本文仅供交流学习 , 版权归属原作者。温馨提示:《Java后端栈》推文内容如有侵权请您告知我们会在第一时间处理或撤销;互联网是一个资源共享的生态圈,我们崇尚分享。好文推荐:


欢迎添加栈哥个人微信 ysle007  进粉丝群或围观朋友圈

扫码关注带你吊爆Java后端技术

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存