当前位置: 首页 > news >正文

springsecurity不拦截某个接口_Spring Security (一):Simple Demo

Simple Demo

该系列都是基于前后端分离的方式,返回的数据都是使用的 JSON,以及使用了自定义的返回结果 starter:https://gitee.com/lin-mt/result-spring-boot。

源码地址: https://gitee.com/lin-mt/spring-boot-examples/tree/master/spring-security-data-permission-control

新建一个 SpringBoot 项目,引入相关依赖

<dependency>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <groupId>org.springframework.boot</groupId>
</dependency>
<dependency>
    <artifactId>spring-boot-starter-security</artifactId>
    <groupId>org.springframework.boot</groupId>
</dependency>
<dependency>
    <artifactId>spring-boot-starter-web</artifactId>
    <groupId>org.springframework.boot</groupId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>

<dependency>
    <artifactId>mysql-connector-java</artifactId>
    <groupId>mysql</groupId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>com.gitee.lin-mt</groupId>
    <artifactId>result-spring-boot-starter</artifactId>
</dependency>

自定义用户信息

/**
 * 用户信息.
 *
 * @author <a href="mailto:lin-mt@outlook.com">lin-mt</a>
 */
@Entity
@Table(name = "sys_user")
public class SysUser extends BaseEntity implements UserDetails, CredentialsContainer {
    
    private String username;
    
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private String secretCode;
    
    private int accountNonExpired;
    
    private int accountNonLocked;
    
    private int credentialsNonExpired;
    
    private int enabled;
    
    @Transient
    private Collection<? extends GrantedAuthority> authorities;
    
    // setter and getter

    @Basic
    @Override
    @Column(name = "username")
    public String getUsername() {
        return username;
    }
    
    @Override
    @Transient
    @JsonIgnore
    public String getPassword() {
        return getSecretCode();
    }
    
    @Override
    @Transient
    public boolean isAccountNonExpired() {
        return 0 == this.accountNonExpired;
    }
    
    @Override
    @Transient
    public boolean isAccountNonLocked() {
        return 0 == this.accountNonLocked;
    }
    
    @Override
    @Transient
    public boolean isCredentialsNonExpired() {
        return 0 == this.credentialsNonExpired;
    }
    
    @Override
    @Transient
    public boolean isEnabled() {
        return 1 == this.enabled;
    }
    
    @Override
    public void eraseCredentials() {
        this.secretCode = null;
    }
    
    @Override
    public String toString() {
        return "SysUser{" + "username='" + username + ''' + ", gender='" + gender + ''' + ", phoneNumber='"
                + phoneNumber + ''' + ", emailAddress='" + emailAddress + ''' + ", accountNonExpired="
                + accountNonExpired + ", accountNonLocked=" + accountNonLocked + ", credentialsNonExpired="
                + credentialsNonExpired + ", enabled=" + enabled + ", authorities=" + authorities + '}';
    }
}
  1. 为什么要实现接口 org.springframework.security.core.userdetails.UserDetails 呢?

首先,Spring Security 肯定需要根据用户输入的某个条件(通常是用户名,也就是 username )获取该条件对应的用户信息,然后再根据登录人输入的信息以及对应的用户信息去验证是否能够登录系统。那么 Spring Security 怎么才能从用户信息中获取验证所需要的数据呢,用户信息是我们返回给 Spring Security 的,无论是从内存还是数据库获取,都是包装成一个实体。重点来了,如果这个实体实现了某个接口,那么就可以将该实体向上转型为该接口的实体(这是 Java 基础哈),这时候就可以直接调用实体中接口的方法获取实体的数据!然后就可以根据这些数据验证登录人能不能进入系统了,所以 UserDetails 接口中我们要实现的几个方法中,返回的数据就是 Spring Security 用来验证的数据。

  1. 为什么实现接口 org.springframework.security.core.CredentialsContainer 呢?

网上很多的博客都没有实现该接口,这点被忽略了。在我们成功登陆之后,Spring Security 需要把我们的登陆信息存储起来,这样我们下次访问的时候才不需要重复校验,在第 1 点中有提到,返回的用户信息可以是我们自定义的 ,那么为了防止数据泄漏(我们可能需要把存储的用户信息返回给前端),存储的时候需要隐藏一些敏感信息,而 Spring Security 又不清楚你自定义的信息中有哪些字段需要隐藏,那么就提供一个接口,在存储信息的时候调用该接口隐藏信息的方法,隐藏哪些信息取决于我们在下面这个方法中做什么,Spring Security 会在缓存用户信息的时候调用该方法,如果你实现了 CredentialsContainer 的方法的话(如果我不懒的话,后续会有博客分析是在哪里调用的,也可以自己点源码看下)。

@Override
public void eraseCredentials() {
}
  1. 根据登录人输入的条件获取用户信息.

这就是 CURD 中的 Read 了(一丝丝熟悉的味道扑面而来),这部分需要我们自己实现,Spring Security 提供了几种实现,包括从内存中读取的 InMemoryUserDetailsManager,从数据库读取数据的 JdbcUserDetailsManager,当然我们也可以如下自定义实现接口 org.springframework.security.core.userdetails.UserDetailsService:

/**
 * 用户 Service 实现类.
 *
 * @author <a href="mailto:lin-mt@outlook.com">lin-mt</a>
 */
@Service
public class SysUserServiceImpl implements SysUserService {
    
    private final SysUserRepository userRepository;
        
    private final SysUserRoleRepository userRoleRepository;
        
    private final SysRoleRepository roleRepository;
        
    private final PasswordEncoder passwordEncoder;
        
    public SysUserServiceImpl(SysUserRepository userRepository, SysUserRoleRepository userRoleRepository,
                SysRoleRepository roleRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.userRoleRepository = userRoleRepository;
        this.roleRepository = roleRepository;
        this.passwordEncoder = passwordEncoder;
    }
        
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser user = userRepository.getByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        List<SysUserRole> sysUserRoles = userRoleRepository.findByUserId(user.getId());
        if (!CollectionUtils.isEmpty(sysUserRoles)) {
            Set<Long> roleIds = sysUserRoles.stream().map(SysUserRole::getRoleId).collect(Collectors.toSet());
            user.setAuthorities(roleRepository.findAllById(roleIds));
        }
        return user;
    }
    
}

注入的时候,@Service 最好是加上别名,因为 Spring Security 在 UserDetailsServiceAutoConfiguration 中默认条件注入了一个 InMemoryUserDetailsManager,所以,如果不取别名,在使用的时候就不知道是使用的哪个 UserDetialsService 的实现了。

自定义从请求中获取登录信息的方式

Spring Security 默认是从 form 表单中获取 username 和 password,但是我们使用的是 json 方式提交数据的,所以从 request 中获取登录信息就需要我们自定义实现:

/**
 * 处理使用 Json 格式数据的登陆方式.
 *
 * @author <a href="mailto:lin-mt@outlook.com">lin-mt</a>
 */
@Component
public class JsonAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    
    @Autowired
    public JsonAuthenticationFilter(ResultAuthenticationSuccessHandler authenticationSuccessHandler,
            ResultAuthenticationFailureHandler authenticationFailureHandler,
            @Lazy AuthenticationManager authenticationManager) {
        // 自定义该方式处理登录信息的登录地址,默认是 /login POST
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/loginByJson", "POST"));
        setAuthenticationSuccessHandler(authenticationSuccessHandler);
        setAuthenticationFailureHandler(authenticationFailureHandler);
        setAuthenticationManager(authenticationManager);
    }
    
    @Override
    public Authentication attemptAuthentication(final HttpServletRequest request, final HttpServletResponse response)
            throws AuthenticationException {
        if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
            final ObjectMapper mapper = new ObjectMapper();
            UsernamePasswordAuthenticationToken authToken = null;
            try (final InputStream inputStream = request.getInputStream()) {
                final SysUser user = mapper.readValue(inputStream, SysUser.class);
                authToken = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
            } catch (final IOException e) {
                authToken = new UsernamePasswordAuthenticationToken("", "");
                throw new AuthenticationServiceException("Failed to read data from request", e.getCause());
            } finally {
                setDetails(request, authToken);
            }
            // 进行登录信息的验证
            return this.getAuthenticationManager().authenticate(authToken);
        } else {
            return super.attemptAuthentication(request, response);
        }
    }
}

配置

SpringSecurityConfig

在该配置中,我们自定义实现了登陆成功后的返回数据、登录失败后的返回数据、权限认证失败后的返回数据,同时设置 Spring Security 读取用户信息的方式(我们上一步自定义的 UserServiceDetails 实现)。

/**
 * Spring Security 配置.
 *
 * @author <a href="mailto:lin-mt@outlook.com">lin-mt</a>
 */
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    
    private final SysUserService userService;
    
    private final JsonAuthenticationFilter jsonAuthenticationFilter;
    
    private final ResultAccessDeniedHandler accessDeniedHandler;
    
    public SpringSecurityConfig(SysUserService userService, JsonAuthenticationFilter jsonAuthenticationFilter,
            ResultAccessDeniedHandler accessDeniedHandler) {
        this.userService = userService;
        this.jsonAuthenticationFilter = jsonAuthenticationFilter;
        this.accessDeniedHandler = accessDeniedHandler;
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        http.csrf().disable()
                .formLogin()
                    .loginPage("/login")
                    .permitAll()
                .and()
                .authorizeRequests()
                    .mvcMatchers("/user/register").permitAll()
                .anyRequest().authenticated()
                .and()
                .exceptionHandling()
                    .accessDeniedHandler(accessDeniedHandler);
        // @formatter:on
        http.addFilterAt(jsonAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
    
    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
}

ApplicationConfig

配置密码加密方式,我们配置的密码加密方式,要跟我们注册用户时的加密密码的方式一样,Spring Security 提供以下多种密码加密的方式,我们就选择其中的 DelegatingPasswordEncoder:

/**
 * 应用配置.
 *
 * @author <a href="mailto:lin-mt@outlook.com">lin-mt</a>
 */
@Configuration(proxyBeanMethods = false)
public class ApplicationConfig {
    
    /**
     * 注入密码加密方式.
     *
     * @return 密码加密方式
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

8edd402159dbe2bd31dd30c79663e6da.png

ApplicationController

/**
 * ApplicationController.
 *
 * @author <a href="mailto:lin-mt@outlook.com">lin-mt</a>
 */
@RestController
public class ApplicationController {
    
    /**
     * <p>
     * 需要拥有 ROLE_admin 权限才能访问的接口,此处设置权限时,如果是hasAnyRole,不需要以 ROLE_ 开头,在验证是否有权限的时候, 如果没有前缀,会自动加上前缀然后进行验证.
     * </p>
     *
     * @return Result
     */
    @GetMapping("/admin")
    @PreAuthorize("hasAnyRole('admin')")
    public Result<Object> admin() {
        return Result.success().setMessage("This is admin index.");
    }
    
    /**
     * <p>需要拥有 ROLE_user 权限才能访问的接口.</p>
     *
     * @return Result
     */
    @GetMapping("/user")
    @PreAuthorize("hasAnyRole('user')")
    public Result<Object> user() {
        return Result.success().setMessage("This is user index.");
    }
    
}

测试

登录测试

在 SysUserServiceImpl 可以自己实现伪代码,初始化用户名以及用户拥有的权限,同时在ApplicationController 中添加相关的接口以测试。

①:我们自定义的处理 json 登录方式的地址 ②:用户名和密码 ③:自定义登录成功的返回数据

7b02bfb54fa09e403aea82607679e798.png

权限测试

4b11e5f26a778720ccdc59acb661a178.png

相关文章:

  • enityframework 已连接的当前状态为打开。_Http 持久连接与 HttpClient 连接池,有哪些不为人知的关系?...
  • 脚本录制软件python 按键精灵 tc_从10种脚相看你的财运
  • 用eviews计算产出弹性_深圳竞价优化|投放都和产出差不多了,还有人在投竞价...
  • qq登录界面句柄_天天玩QQ!知道登录界面那两个人是谁吗?网友:不是情侣?...
  • led数字字体_led显示屏知识大全
  • python设置单元格宽度_ms-word – Python-docx,如何在表中设置单元格宽度?
  • c++判断整数翻转溢出_CBC字节翻转攻击解析
  • python调用数据库存储过程_Mysql学习---使用Python执行存储过程
  • python实现中值滤波_Python 实现中值滤波、均值滤波
  • bigdecimal不保留小数_深入理解 BigDecimal
  • mysql 去重复查询_MySQL事务隔离级别和实现原理(看这一篇文章就够了!)
  • matlab追赶法解三对角方程组_高斯消元法解线性方程组
  • case when then else_第6章 函数、谓词、CASE表达式及练习题
  • git add 撤销_Git中的各种后悔药
  • python 爬取实时数据django显示_django+echart数据动态显示的例子
  • python3.6+scrapy+mysql 爬虫实战
  • 《Javascript高级程序设计 (第三版)》第五章 引用类型
  • JavaScript 事件——“事件类型”中“HTML5事件”的注意要点
  • JavaScript设计模式与开发实践系列之策略模式
  • js操作时间(持续更新)
  • JS正则表达式精简教程(JavaScript RegExp 对象)
  • PHP 7 修改了什么呢 -- 2
  • SAP云平台运行环境Cloud Foundry和Neo的区别
  • 阿里云ubuntu14.04 Nginx反向代理Nodejs
  • 动态规划入门(以爬楼梯为例)
  • 工程优化暨babel升级小记
  • 关于 Cirru Editor 存储格式
  • 面试题:给你个id,去拿到name,多叉树遍历
  • 模仿 Go Sort 排序接口实现的自定义排序
  • 浅谈web中前端模板引擎的使用
  • 实现简单的正则表达式引擎
  • 小程序 setData 学问多
  • 源码之下无秘密 ── 做最好的 Netty 源码分析教程
  • Nginx惊现漏洞 百万网站面临“拖库”风险
  • 阿里云重庆大学大数据训练营落地分享
  • #Linux杂记--将Python3的源码编译为.so文件方法与Linux环境下的交叉编译方法
  • #stm32整理(一)flash读写
  • $$$$GB2312-80区位编码表$$$$
  • (14)Hive调优——合并小文件
  • (2009.11版)《网络管理员考试 考前冲刺预测卷及考点解析》复习重点
  • (C语言)共用体union的用法举例
  • (day 12)JavaScript学习笔记(数组3)
  • (Java实习生)每日10道面试题打卡——JavaWeb篇
  • (二)Pytorch快速搭建神经网络模型实现气温预测回归(代码+详细注解)
  • (二)windows配置JDK环境
  • (论文阅读23/100)Hierarchical Convolutional Features for Visual Tracking
  • (十八)devops持续集成开发——使用docker安装部署jenkins流水线服务
  • (十三)Maven插件解析运行机制
  • (提供数据集下载)基于大语言模型LangChain与ChatGLM3-6B本地知识库调优:数据集优化、参数调整、Prompt提示词优化实战
  • (原創) 如何刪除Windows Live Writer留在本機的文章? (Web) (Windows Live Writer)
  • (原創) 如何優化ThinkPad X61開機速度? (NB) (ThinkPad) (X61) (OS) (Windows)
  • (转)拼包函数及网络封包的异常处理(含代码)
  • .bat批处理(四):路径相关%cd%和%~dp0的区别
  • .net core 6 redis操作类
  • .net 写了一个支持重试、熔断和超时策略的 HttpClient 实例池