SpringSecurity入门(二)

news2024/11/18 15:18:43

8、获取用户认证信息

image.png

  • 三种策略模式,调整通过修改VM options
// 如果没有设置自定义的策略,就采用MODE_THREADLOCAL模式
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
// 采用InheritableThreadLocal,它是ThreadLocal的一个子类,适用多线程的环境
public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
// 全局策略,实现方式就是static SecurityContext contextHolder
public static final String MODE_GLOBAL = "MODE_GLOBAL";
  • image.png
-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL

image.png

8.1、 使用代码获取

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User) authentication.getPrincipal();
System.out.println("身份信息:" + authentication.getPrincipal());
System.out.println("用户:" + user.getUsername());
System.out.println("权限信息:" + authentication.getAuthorities());

image.png

8.2、 前端页面获取

8.2.1、 引入依赖

  • 不需要版本号
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>

8.2.2、 导入命名空间,获取数据

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>login</title>
</head>
<body>
<form th:action="@{/bb}" method="post">
    <p>
        <input type="submit" value="注销登陆">
    </p>
</form>

<hr>

<h2>获取认证用户信息</h2>

<ul>
<!--     <li sec:authentication="name"></li>-->
<!--     <li sec:authentication="authorities"></li>-->
<!--     <li sec:authentication="credentials"></li>-->
<!--     <li sec:authentication="authenticated"></li>-->
     <li sec:authentication="principal.username"></li>
     <li sec:authentication="principal.authorities"></li>
     <li sec:authentication="principal.accountNonExpired"></li>
     <li sec:authentication="principal.accountNonLocked"></li>
     <li sec:authentication="principal.credentialsNonExpired"></li>
</ul>


</body>
</html>

image.png

9、 自定义数据源

9.1、 流程分析

image.png
When the user submits their credentials, the AbstractAuthenticationProcessingFilter creates an Authentication from the HttpServletRequest to be authenticated. The type of Authentication created depends on the subclass of AbstractAuthenticationProcessingFilter. For example, UsernamePasswordAuthenticationFilter creates a UsernamePasswordAuthenticationToken from a username and password that are submitted in the HttpServletRequest.
Next, the Authentication is passed into the AuthenticationManager to be authenticated.
If authentication fails, then Failure

  • The SecurityContextHolder is cleared out.
  • RememberMeServices.loginFail is invoked. If remember me is not configured, this is a no-op.
  • AuthenticationFailureHandler is invoked.

If authentication is successful, then Success.

  • SessionAuthenticationStrategy is notified of a new log in.
  • The Authentication is set on the SecurityContextHolder. Later the SecurityContextPersistenceFilter saves the SecurityContext to the HttpSession.
  • RememberMeServices.loginSuccess is invoked. If remember me is not configured, this is a no-op.
  • ApplicationEventPublisher publishes an InteractiveAuthenticationSuccessEvent.
  • AuthenticationSuccessHandler is invoked.

image.png
image.png
image.png

9.2、 修改WebSecurityConfigurer

  @Autowired
    DataSource dataSource;


    @Bean
    public PasswordEncoder bcryptPasswordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }


    public UserDetailsService users(PasswordEncoder encoder) {
        // The builder will ensure the passwords are encoded before saving in memory
        UserDetails user = User.withUsername("user")
                .password(encoder.encode("123"))
                .roles("USER")
                .build();
        UserDetails admin = User.withUsername("admin")
                .password(encoder.encode("123"))
                .roles("USER", "ADMIN")
                .build();
        return new InMemoryUserDetailsManager(user, admin);
    }


    public UserDetailsService users() {
        JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
//        UserDetails admin = User.withUsername("齐丰")
//                .password(encoder.encode("123456"))
//                .roles("USER")
//                .build();
//        users.createUser(admin);
        System.out.println(dataSource.getClass());
        return users;
    }

9.3、 In-Memory Authentication

  • 修改SecurityFilterChain
  • 通过指定userDetailsService来切换不同的认证数据储存方式
    @Bean
    @SuppressWarnings("all")
    SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        return
                //开启权限验证
                httpSecurity
                        .authorizeRequests()
                        //permitAll直接放行,必须在anyRequest().authenticated()前面
                        .mvcMatchers("/toLogin").permitAll()
                        .mvcMatchers("/index").permitAll()
                        //anyRequest所有请求都需要认证
                        .anyRequest().authenticated()
                        .and()
                        //使用form表单验证
                        .formLogin()
                        //自定义登陆页面
                        .loginPage("/toLogin")
                        //自定义登陆页面后必须指定处理登陆请求的url
                        .loginProcessingUrl("/doLogin")
//               自定义接收用户名的参数名为uname
                        .usernameParameter("uname")
//               自定义接收密码的参数名为pwd
                        .passwordParameter("pwd")
//               登陆认证成功后跳转的页面(转发),必须使用POST请求
//                .successForwardUrl("/test")
//               陆认证成功后跳转的页面(转发),必须使用GET请求
//                 .defaultSuccessUrl("/test",true)
                        //不会每次都跳转定义的页面,默认会记录认证拦截的请求,如果是拦截的受限资源会优先跳转到之前被拦截的请求。需要每次都跳转使defaultSuccessUrl("/test",true)
//                 .defaultSuccessUrl("/test")
//                前后端分离时代自定义认证成功处理
                        .successHandler(new MyAuthenticationSuccessHandler())
//               认证失败跳转页面,必须使用POST请求
//                .failureForwardUrl("/toLogin")
//               认证失败跳转页面,必须使用GET请求
//                 .failureUrl("/toLogin")
//               前后端分离时代自定义认证失败处理
                        .failureHandler(new MyAuthenticationFailureHandler())
                        .and()
//               注销
                        .logout()
//               指定默认注销url,默认请求方式GET
//                .logoutUrl("/logout")
//               自定义注销url
                        .logoutRequestMatcher(new OrRequestMatcher(
                                new AntPathRequestMatcher("/aa", "GET"),
                                new AntPathRequestMatcher("/bb", "POST")
                        ))
//               销毁session,默认为true
                        .invalidateHttpSession(true)
//               清除认证信息,默认为true
                        .clearAuthentication(true)
//               注销成功后跳转页面
//                .logoutSuccessUrl("/toLogin")
                        .logoutSuccessHandler(new MyLogoutSuccessHandler())
                        .and()
                        .userDetailsService(users(bcryptPasswordEncoder()))
                        // .userDetailsService(users())
                        //禁止csrf跨站请求保护
                        .csrf().disable()
                        .build();
    }

9.4、 JDBC Authentication

9.3.1、 引入依赖

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.11</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency>

9.3.2、 数据库表及连接地址配置

  • 表结构官方文档
org/springframework/security/core/userdetails/jdbc/users.ddl

image.png

  • 数据库连连配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.0.0.128:3306/test
    username: wq
    password: 123456

9.3.3、 修改SecurityFilterChain

  • 通过指定userDetailsService来切换不同的认证数据储存方式
@Configuration
    public class WebSecurityConfigurer {

        @Autowired
        DataSource dataSource;

        @Bean
        public PasswordEncoder bcryptPasswordEncoder() {
            return PasswordEncoderFactories.createDelegatingPasswordEncoder();
        }

        public UserDetailsService inMemoryUsers(PasswordEncoder encoder) {
            // The builder will ensure the passwords are encoded before saving in memory
            UserDetails user = User.withUsername("user")
                .password(encoder.encode("123"))
                .roles("USER")
                .build();
            UserDetails admin = User.withUsername("admin")
                .password(encoder.encode("123"))
                .roles("USER", "ADMIN")
                .build();
            return new InMemoryUserDetailsManager(user, admin);
        }


        public UserDetailsService jdbcUsers(PasswordEncoder encoder) {
            JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
            //        UserDetails admin = User.withUsername("齐丰")
            //                .password(encoder.encode("123456"))
            //                .roles("USER")
            //                .build();
            //        users.deleteUser(admin.getUsername());
            //        users.createUser(admin);
            System.out.println(dataSource.getClass());
            return users;
        }


        @Bean
        @SuppressWarnings("all")
        SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
            return
                //开启权限验证
                httpSecurity
                .authorizeRequests()
                //permitAll直接放行,必须在anyRequest().authenticated()前面
                .mvcMatchers("/toLogin").permitAll()
                .mvcMatchers("/index").permitAll()
                //anyRequest所有请求都需要认证
                .anyRequest().authenticated()
                .and()
                //使用form表单验证
                .formLogin(login ->
                           login
                           //自定义登陆页面
                           .loginPage("/toLogin")
                           //自定义登陆页面后必须指定处理登陆请求的url
                           .loginProcessingUrl("/doLogin")
                           // 自定义接收用户名的参数名为uname
                           .usernameParameter("uname")
                           //自定义接收密码的参数名为pwd
                           .passwordParameter("pwd")
                           // 认证成功后跳转的页面(转发),必须使用POST请求
                           //.successForwardUrl("/test")
                           // 证成功后跳转的页面(重定向),必须使用GET请求
                           //.defaultSuccessUrl("/test")
                           //不会每次都跳转定义的页面,默认会记录认证拦截的请求,如果是拦截的受限资源会优先跳转到之前被拦截的请求。需要每次都跳转使defaultSuccessUrl("/test",true)
                           //.defaultSuccessUrl("/test",true)
                           //前后端分离时代自定义认证成功处理
                           .successHandler(new MyAuthenticationSuccessHandler())
                           //前后端分离时代自定义认证失败处理
                           .failureHandler(new MyAuthenticationFailureHandler())
                          )
                //注销
                .logout(logout -> {
                    logout
                        //指定默认注销url,默认请求方式GET
                        //.logoutUrl("/logout")
                        .logoutRequestMatcher(
                            //自定义注销url
                            new OrRequestMatcher(
                                new AntPathRequestMatcher("/aa", "GET"),
                                new AntPathRequestMatcher("/bb", "POST")))
                        //注销成功后跳转页面
                        //.logoutSuccessUrl("/toLogin")
                        //前后端分离时代自定义注销登录处理器
                        .logoutSuccessHandler(new MyLogoutSuccessHandler())
                        //销毁session,默认为true
                        .invalidateHttpSession(true)
                        //清除认证信息,默认为true
                        .clearAuthentication(true);
                })
                //指定UserDetailsService来切换认证信息不同的存储方式(数据源)
                .userDetailsService(inMemoryUsers(bcryptPasswordEncoder()))
                .userDetailsService(jdbcUsers(bcryptPasswordEncoder()))
                //禁止csrf跨站请求保护
                .csrf().disable()
                .build();
        }
    }
方式一:通过指定userDetailsService来切换不同的认证数据储存方式,也可同时指定多个如:
.userDetailsService(users(bcryptPasswordEncoder()))
.userDetailsService(users())
方式二:在指定自定义的UserDetailsService上加上@Bean注解,或者实现UserDetailsService接口

image.png

9.5、扩展JDBC Authentication之mysql

表设计

image.pngimage.pngimage.png

-- test1.`user` definition

CREATE TABLE `user` (
  `userId` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(100) NOT NULL COMMENT '用户名',
  `password` varchar(500) NOT NULL COMMENT '密码',
  `accountNonExpired` tinyint(1) NOT NULL,
  `enabled` tinyint(1) NOT NULL,
  `accountNonLocked` tinyint(1) NOT NULL,
  `credentialsNonExpired` tinyint(1) NOT NULL,
  PRIMARY KEY (`userId`),
  UNIQUE KEY `user_UN` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- test1.`role` definition

CREATE TABLE `role` (
  `roleId` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL,
  `name_zh` varchar(100) NOT NULL,
  PRIMARY KEY (`roleId`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- test1.`role` definition

CREATE TABLE `role` (
  `roleId` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL,
  `name_zh` varchar(100) NOT NULL,
  PRIMARY KEY (`roleId`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

INSERT INTO test1.`user` (username,password,accountNonExpired,enabled,accountNonLocked,credentialsNonExpired) VALUES
	 ('root','{noop}123',1,1,1,1),
	 ('admin','{noop}123',1,1,1,1),
	 ('qifeng','{noop}123',1,1,1,1);

INSERT INTO test1.`role` (name,name_zh) VALUES
	 ('ROLE_product','商品管理员'),
	 ('ROLE_admin','系统管理员'),
	 ('ROLE_user','用户管理员');

INSERT INTO test1.user_role (userId,roleId) VALUES
	 (1,1),
	 (1,2),
	 (2,2),
	 (3,3);

9.5.1、引入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.11</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

9.5.2、配置数据库连接

server:
  port: 8081
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.0.0.128:3306/test1
    username: wq
    password: 123456
mybatis:
  type-aliases-package: com.wanqi.pojo
  mapper-locations: classpath:mapper/*.xml

9.5.3、编写实体类与Mapper

User
  • User继承UserDetails方便扩展
public class User implements UserDetails {
    private Long userId;
    private String username;
    private String password;
    /** 账户是否过期 */
    private Boolean accountNonExpired;
    /** 账户是否激活 */
    private Boolean enabled;
    /** 账户是否被锁定 */
    private Boolean accountNonLocked;
    /** 密码是否过期 */
    private Boolean credentialsNonExpired;
    private List<Role> roles = new ArrayList<>();

    public User() {
    }

    public User(String username, String password, Boolean accountNonExpired, Boolean enabled, Boolean accountNonLocked, Boolean credentialsNonExpired, List<Role> roles) {
        this.username = username;
        this.password = password;
        this.accountNonExpired = accountNonExpired;
        this.enabled = enabled;
        this.accountNonLocked = accountNonLocked;
        this.credentialsNonExpired = credentialsNonExpired;
        this.roles = roles;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Set<SimpleGrantedAuthority> authorities = new HashSet<>();
        roles.forEach(role -> authorities.add(new SimpleGrantedAuthority(role.getName())));
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setAccountNonExpired(Boolean accountNonExpired) {
        this.accountNonExpired = accountNonExpired;
    }

    public void setEnabled(Boolean enabled) {
        this.enabled = enabled;
    }

    public void setAccountNonLocked(Boolean accountNonLocked) {
        this.accountNonLocked = accountNonLocked;
    }

    public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
        this.credentialsNonExpired = credentialsNonExpired;
    }
}
@Mapper
public interface UserMapper {
   /**
    * 根据用户名查询用户
    */
   User loadUserByUsername(@Param("username") String username);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wanqi.mapper.UserMapper">
<select id="loadUserByUsername" parameterType="String" resultType="user">
    select * from user where username=#{username}
</select>
</mapper>
Role
public class Role {
    private Long roleId;
    private String name;
    private String name_zh;

    public Long getRoleId() {
        return roleId;
    }

    public void setRoleId(Long roleId) {
        this.roleId = roleId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName_zh() {
        return name_zh;
    }

    public void setName_zh(String name_zh) {
        this.name_zh = name_zh;
    }

    public Role() {
    }

    public Role(String name, String name_zh) {
        this.name = name;
        this.name_zh = name_zh;
    }
}
@Mapper
public interface RoleMapper {
    /**
     * 根据用户编号查询角色信息
     */
    List<Role> getRoleByUserId(@Param("userId") Long userId);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wanqi.mapper.RoleMapper">

<select id="getRoleByUserId" parameterType="Long" resultType="com.wanqi.pojo.Role">
    select r.roleId,
           r.name,
           r.name_zh
    from role r,user_role ur
    where r.roleId = ur.roleId
    and  ur.userId= #{userId}
</select>
</mapper>

9.5.4、实现UserDetailsService接口

@Component("userDetailsImpl")
public class UserDetailsImpl implements UserDetailsService {
    @Autowired
   private UserMapper userMapper;
    @Autowired
   private RoleMapper roleMapper;

    @Override
    public org.springframework.security.core.userdetails.UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.loadUserByUsername(username);
        if(user == null){
            throw new UsernameNotFoundException("用户名不存在");
        }
        List<Role> roles = roleMapper.getRoleByUserId(user.getUserId());
        user.setRoles(roles);
        return user;
    }
}

9.5.5、指定认证数据源

  • 通过 httpSecurity.userDetailsService(userDetailsImpl)
@Configuration
    public class MyWebSecurityConfigurer {

        private UserDetailsImpl userDetailsImpl;

        @Autowired
        public void setUserDetailsImpl(UserDetailsImpl userDetailsImpl) {
            this.userDetailsImpl = userDetailsImpl;
        }

        @Bean
        @SuppressWarnings("all")
        SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
            return
                //开启权限验证
                httpSecurity
                .authorizeRequests()
                //permitAll直接放行,必须在anyRequest().authenticated()前面
                .mvcMatchers("/toLogin").permitAll()
                .mvcMatchers("/index").permitAll()
                //anyRequest所有请求都需要认证
                .anyRequest().authenticated()
                .and()
                //使用form表单验证
                .formLogin(login ->
                           login
                           //自定义登陆页面
                           .loginPage("/toLogin")
                           //自定义登陆页面后必须指定处理登陆请求的url
                           .loginProcessingUrl("/doLogin")
                           // 自定义接收用户名的参数名为uname
                           .usernameParameter("uname")
                           //自定义接收密码的参数名为pwd
                           .passwordParameter("pwd")
                           // 认证成功后跳转的页面(转发),必须使用POST请求
                           //.successForwardUrl("/test")
                           // 证成功后跳转的页面(重定向),必须使用GET请求
                           //.defaultSuccessUrl("/test")
                           //不会每次都跳转定义的页面,默认会记录认证拦截的请求,如果是拦截的受限资源会优先跳转到之前被拦截的请求。需要每次都跳转使defaultSuccessUrl("/test",true)
                           //.defaultSuccessUrl("/test",true)
                           //前后端分离时代自定义认证成功处理
                           .successHandler(new MyAuthenticationSuccessHandler())
                           //前后端分离时代自定义认证失败处理
                           .failureHandler(new MyAuthenticationFailureHandler())
                          )
                //注销
                .logout(logout -> {
                    logout
                        //指定默认注销url,默认请求方式GET
                        //.logoutUrl("/logout")
                        .logoutRequestMatcher(
                            //自定义注销url
                            new OrRequestMatcher(
                                new AntPathRequestMatcher("/aa", "GET"),
                                new AntPathRequestMatcher("/bb", "POST")))
                        //注销成功后跳转页面
                        //.logoutSuccessUrl("/toLogin")
                        //前后端分离时代自定义注销登录处理器
                        .logoutSuccessHandler(new MyLogoutSuccessHandler())
                        //销毁session,默认为true
                        .invalidateHttpSession(true)
                        //清除认证信息,默认为true
                        .clearAuthentication(true);
                })
                //指定UserDetailsService来切换认证信息不同的存储方式(数据源)
                .userDetailsService(userDetailsImpl)
                //禁止csrf跨站请求保护
                .csrf().disable()
                .build();
        }
    }

10、自定义JSON认证,前后端分离

10.1、HttpSecurity过滤器方法介绍

/*
* at:用指定的filter替换过滤器链中的指定filter
* before:放在过滤器链中哪一个filter之前
* after:放在过滤器链中哪一个filter之后
* httpSecurity.addFilterAt();
* httpSecurity.addFilterBefore(, );
* httpSecurity.addFilterAfter();
* */

10.2、自定义登录处理filter

public class JsonLoginFilter extends UsernamePasswordAuthenticationFilter {

    public JsonLoginFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        System.out.println("JosnLoginFilter");
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
            try {
                Map<String,String> map = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                String username = map.get(getUsernameParameter());
                username = (username != null) ? username.trim() : "";
                String password = map.get(getPasswordParameter());
                password = (password != null) ? password : "";
                UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
                        password);
                System.out.println(username);
                System.out.println(password);
                setDetails(request, authRequest);
                return getAuthenticationManager().authenticate(authRequest);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return super.attemptAuthentication(request, response);
    }
}

10.3、配置文件编写

@Configuration
@EnableWebSecurity
public class WebSecurityConfigurer {

    private UserDetailsImpl userDetailsImpl;

    @Autowired
    public void setUserDetailsImpl(UserDetailsImpl userDetailsImpl) {
        this.userDetailsImpl = userDetailsImpl;
    }

    @Bean
    public PasswordEncoder bcryptPasswordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Autowired
    AuthenticationConfiguration authenticationConfiguration;
    /**
     * 获取AuthenticationManager(认证管理器),登录时认证使用
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public JsonLoginFilter josnLoginFilter() throws Exception {
        System.out.println("setAuthenticationManager");
        JsonLoginFilter filter = new JsonLoginFilter(authenticationManager());
        filter.setUsernameParameter("uname");
        filter.setPasswordParameter("pwd");
        //前后端分离时代自定义认证成功处理
        filter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
        //前后端分离时代自定义认证失败处理
        filter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
        return filter;
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        return
                //开启权限验证
                httpSecurity
                        .authorizeRequests()
                        //permitAll直接放行,必须在anyRequest().authenticated()前面
                        .mvcMatchers("/index").permitAll()
                        //anyRequest所有请求都需要认证
                        .anyRequest().authenticated()
                        .and()
                        //使用form表单验证
                        .formLogin()
                        .and()
                        .addFilterAt(josnLoginFilter(),UsernamePasswordAuthenticationFilter.class)
                        //注销
                        .logout(logout -> {
                            logout
                                    .logoutRequestMatcher(
                                            //自定义注销url
                                            new OrRequestMatcher(
                                                    new AntPathRequestMatcher("/aa", "GET"),
                                                    new AntPathRequestMatcher("/bb", "POST")))
                                    //前后端分离时代自定义注销登录处理器
                                    .logoutSuccessHandler(new MyLogoutSuccessHandler())
                                    //销毁session,默认为true
                                    .invalidateHttpSession(true)
                                    //清除认证信息,默认为true
                                    .clearAuthentication(true);
                        })
                        //指定UserDetailsService来切换认证信息不同的存储方式(数据源)
                        .userDetailsService(userDetailsImpl)
                        .exceptionHandling()
                        .authenticationEntryPoint((request, response, authException) -> {
//                            response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                            response.setHeader("content-type", "application/json;charset=UTF-8");
                            response.setStatus(HttpStatus.UNAUTHORIZED.value());
                            response.getWriter().write("请先认证后重试!");
                        })
                        .and()
                        //禁止csrf跨站请求保护
                        .csrf().disable()
                        .build();
    }
}

10.4、使用自定义filter注意事项

httpSecurity.formLogin()必须使用无参的

11、实现kaptcha图片验证码

引入依赖

<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>

1、编写kaptcha配置类

@Configuration
public class KaptchaConfig {
    @Bean
    public Producer kaptchaProducer() {
        Properties properties = new Properties();
        //图片的宽度
        properties.setProperty("kaptcha.image.width", "150");
        //图片的高度
        properties.setProperty("kaptcha.image.height", "50");
        //字体大小
        properties.setProperty("kaptcha.textproducer.font.size", "32");
        //字体颜色(RGB)
        properties.setProperty("kaptcha.textproducer.font.color", "0,0,0");
        //验证码字符的集合
        properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
        //验证码长度(即在上面集合中随机选取几位作为验证码)
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        //图片的干扰样式
        properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise");

        DefaultKaptcha Kaptcha = new DefaultKaptcha();
        Config config = new Config(properties);
        Kaptcha.setConfig(config);
        return Kaptcha;
    }
}

2、自定义异常

public class KaptchaNotMatchException extends AuthenticationException {

    public KaptchaNotMatchException(String msg, Throwable cause) {
        super(msg, cause);
    }

    public KaptchaNotMatchException(String msg) {
        super(msg);
    }
}

11.1、传统web开发方式

3、自定义Filter

public class VerifyCodeFilter extends UsernamePasswordAuthenticationFilter {

    public static final String VERIFY_CODE_KEY = "kaptcha";
    private String kaptchaParameter = VERIFY_CODE_KEY;

    public void setKaptchaParameter(String kaptchaParameter) {
        this.kaptchaParameter = kaptchaParameter;
    }

    public String getKaptchaParameter() {
        return kaptchaParameter;
    }

    public VerifyCodeFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        String verifyCode = request.getParameter(getKaptchaParameter());
        String verifyCodeOld = (String) request.getSession().getAttribute("verifyCode");
        if (StringUtils.hasLength(verifyCode) && StringUtils.hasLength(verifyCodeOld)
                && verifyCode.equalsIgnoreCase(verifyCodeOld)) {
            return super.attemptAuthentication(request, response);
        }
        throw new KaptchaNotMatchException("验证码错误");
    }
}

4、提供生成验证码的接口

private Producer producer;

@Autowired
    public void setProducer(Producer producer) {
    this.producer = producer;
}

@RequestMapping("/vc.img")
    public void img(HttpSession session, HttpServletResponse response) throws IOException {
    String verifyCode = producer.createText();
    BufferedImage image = producer.createImage(verifyCode);
    session.setAttribute("verifyCode",verifyCode);
    ServletOutputStream outputStream = response.getOutputStream();
    response.setContentType(MediaType.IMAGE_JPEG_VALUE);
    ImageIO.write(image, "jpg", outputStream);
}

5、登录页面添加验证码

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>login</title>
</head>
<body>
<p th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}"></p>
<form th:action="@{/doLogin}" method="post">
    <p>用户名:<label>
        <input name="uname" type="text"/>
    </label></p>
    <p>密码:<label>
        <input name="pwd" type="password"/>
    </label></p>
    <p>验证码:<label>
        <input name="yzm" type="text"/>
        <img th:src="@{/vc.img}" alt=""/>
    </label></p>
    <p>
        <input type="submit">
    </p>
    <p th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></p>
</form>

</body>
</html>

6、配置WebSecurityConfigurer

	@Configuration
    @EnableWebSecurity
    public class MyWebSecurityConfigurer {

        private UserDetailsImpl userDetailsImpl;

        @Autowired
        public void setUserDetailsImpl(UserDetailsImpl userDetailsImpl) {
            this.userDetailsImpl = userDetailsImpl;
        }
        @Autowired
        AuthenticationConfiguration authenticationConfiguration;

        @Bean
        public PasswordEncoder bcryptPasswordEncoder() {
            return PasswordEncoderFactories.createDelegatingPasswordEncoder();
        }

        /**
        * 获取AuthenticationManager(认证管理器),登录时认证使用
        * @return
        * @throws Exception
        */
        @Bean
        public AuthenticationManager authenticationManager() throws Exception {
            return authenticationConfiguration.getAuthenticationManager();
        }

        public VerifyCodeFilter verifyCodeFilter() throws Exception {
            VerifyCodeFilter filter = new VerifyCodeFilter(authenticationManager());
            // 自定义接收用户名的参数名为uname
            filter.setUsernameParameter("uname");
            //自定义接收密码的参数名为pwd
            filter.setPasswordParameter("pwd");
            filter.setKaptchaParameter("yzm");
            filter.setFilterProcessesUrl("/doLogin");
            //前后端分离时代自定义认证成功处理
            filter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
            //前后端分离时代自定义认证失败处理
            filter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
            return filter;
        }


        @Bean
        @SuppressWarnings("all")
        SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
            return
                //开启权限验证
                httpSecurity
                .authorizeRequests()
                //permitAll直接放行,必须在anyRequest().authenticated()前面
                .mvcMatchers("/toLogin").permitAll()
                .mvcMatchers("/vc.img").permitAll()
                //anyRequest所有请求都需要认证
                .anyRequest().authenticated()
                .and()
                //使用form表单验证
                .formLogin()
                .loginPage("/toLogin")
                .and()
                .addFilterAt(verifyCodeFilter(), UsernamePasswordAuthenticationFilter.class)
                //注销
                .logout(logout -> {
                    logout
                        //指定默认注销url,默认请求方式GET
                        //.logoutUrl("/logout")
                        .logoutRequestMatcher(
                            //自定义注销url
                            new OrRequestMatcher(
                                new AntPathRequestMatcher("/aa", "GET"),
                                new AntPathRequestMatcher("/bb", "POST")))
                        //注销成功后跳转页面
                        .logoutSuccessUrl("/toLogin")
                        //前后端分离时代自定义注销登录处理器
                        //                                    .logoutSuccessHandler(new MyLogoutSuccessHandler())
                        //销毁session,默认为true
                        .invalidateHttpSession(true)
                        //清除认证信息,默认为true
                        .clearAuthentication(true)
                        ;})
                //指定UserDetailsService来切换认证信息不同的存储方式(数据源)
                .userDetailsService(userDetailsImpl)
                //禁止csrf跨站请求保护
                .csrf().disable()
                .build();
        }
    }

11.2、前后端分离方式

3、提供生成验证码的接口

private Producer producer;

@Autowired
    public void setProducer(Producer producer) {
    this.producer = producer;
}

@RequestMapping("/cv.img")
    public String verifyCoder(HttpSession session, HttpServletResponse response) throws IOException {
    String verifyCode = producer.createText();
    BufferedImage image = producer.createImage(verifyCode);
    session.setAttribute("verifyCode",verifyCode);
    FastByteArrayOutputStream outputStream = new FastByteArrayOutputStream();
    ImageIO.write(image, "jpg", outputStream);
    return Base64Utils.encodeToString(outputStream.toByteArray());
}

4、自定义Filter

public class JsonLoginFilter extends UsernamePasswordAuthenticationFilter {
    public static final String VERIFY_CODE_KEY = "kaptcha";
    private String kaptchaParameter = VERIFY_CODE_KEY;

    public void setKaptchaParameter(String kaptchaParameter) {
        this.kaptchaParameter = kaptchaParameter;
    }

    public String getKaptchaParameter() {
        return kaptchaParameter;
    }

    public JsonLoginFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
            try {
                Map<String, String> map = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                String verifyCode = map.get(getKaptchaParameter());
                String verifyCodeOld = (String) request.getSession().getAttribute("verifyCode");
                if (!ObjectUtils.isEmpty(verifyCode) && !ObjectUtils.isEmpty(verifyCodeOld)
                        && verifyCode.equalsIgnoreCase(verifyCodeOld)) {
                    String username = map.get(getUsernameParameter());
                    username = (username != null) ? username.trim() : "";
                    String password = map.get(getPasswordParameter());
                    password = (password != null) ? password : "";
                    UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
                            password);
                    System.out.println(username);
                    System.out.println(password);
                    setDetails(request, authRequest);
                    return getAuthenticationManager().authenticate(authRequest);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        throw new KaptchaNotMatchException("验证码错误");
    }
}

5、配置WebSecurityConfigurer

@Configuration
@EnableWebSecurity
public class WebSecurityConfigurer {

    private UserDetailsImpl userDetailsImpl;

    @Autowired
    public void setUserDetailsImpl(UserDetailsImpl userDetailsImpl) {
        this.userDetailsImpl = userDetailsImpl;
    }

    @Bean
    public PasswordEncoder bcryptPasswordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Autowired
    AuthenticationConfiguration authenticationConfiguration;
    /**
     * 获取AuthenticationManager(认证管理器),登录时认证使用
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public JsonLoginFilter jsonLoginFilter() throws Exception {
        JsonLoginFilter filter = new JsonLoginFilter(authenticationManager());
        filter.setUsernameParameter("uname");
        filter.setPasswordParameter("pwd");
        filter.setKaptchaParameter("yzm");
        //前后端分离时代自定义认证成功处理
        filter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
        //前后端分离时代自定义认证失败处理
        filter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
        return filter;
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        return
                //开启权限验证
                httpSecurity
                        .authorizeRequests()
                        //permitAll直接放行,必须在anyRequest().authenticated()前面
                        .mvcMatchers("/cv.img").permitAll()
                        //anyRequest所有请求都需要认证
                        .anyRequest().authenticated()
                        .and()
                        //使用form表单验证
                        .formLogin()
                        .and()
                        .addFilterAt(jsonLoginFilter(),UsernamePasswordAuthenticationFilter.class)
                        //注销
                        .logout(logout -> {
                            logout
                                    .logoutRequestMatcher(
                                            //自定义注销url
                                            new OrRequestMatcher(
                                                    new AntPathRequestMatcher("/aa", "GET"),
                                                    new AntPathRequestMatcher("/bb", "POST")))
                                    //前后端分离时代自定义注销登录处理器
                                    .logoutSuccessHandler(new MyLogoutSuccessHandler())
                                    //销毁session,默认为true
                                    .invalidateHttpSession(true)
                                    //清除认证信息,默认为true
                                    .clearAuthentication(true);
                        })
                        //指定UserDetailsService来切换认证信息不同的存储方式(数据源)
                        .userDetailsService(userDetailsImpl)
                        .exceptionHandling()
                        .authenticationEntryPoint((request, response, authException) -> {
                            //response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                            response.setHeader("content-type", "application/json;charset=UTF-8");
                            response.setStatus(HttpStatus.UNAUTHORIZED.value());
                            response.getWriter().write("请先认证后重试!");
                        })
                        .and()
                        //禁止csrf跨站请求保护
                        .csrf().disable()
                        .build();
    }
}

相关文章

SpringSecurity入门(一)

SpringSecurity入门(三)

SpringSecurity入门(四)

未完待续

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1811922.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VSFTP本地用户访问-设置

1、本地用户基本配置档设置如下 vim /etc/vsftpd/vstfpd.conf local_enableYES -------允许本地用户登陆 write_enableYES -----允许执行FTP命令&#xff0c;如果禁用&#xff0c;将不能进行上传、下载、删除、重命名等操作 local_umask022 ---------本地用户上传umask值…

公用nacos,实现只调用本机相应服务,不出现负载均衡到别人机器上

当我们有两个研发同时在调试一个微服务模块时&#xff0c;你和对方本地都会启动服务&#xff0c;这就导致在nacos会同时注册两个实例。默认情况下请求这个服务&#xff0c;具体处理请求的程序会在你和对方之间来回轮询&#xff0c;即一下你的服务一下对方的服务。 其结果就导…

重学java 66.IO流 转换流

且敬我疯狂&#xff0c;生命中不败的篇章 —— 24.6.11 一、字符编码 计算机中储存的信息都是用二进制数表示的&#xff0c;而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。[按照某种规则&#xff0c;将字符存储到计算机中&#xff0c;称为编…

10_3、C++继承与派生:派生类成员访问

派生类成员访问 作用域分辨符虚基类概念及用法虚基类派生类的构造函数 赋值兼容规则 作用域分辨符 如果派生类中存在和基类中完全相同的函数&#xff0c;将发生同名覆盖。如果在派生类中存在一个和基类某数据成员同名的数据成员&#xff0c;或者和基类某成员函数的名称和参数表…

采用PHP语言(医院安全不良事件上报系统源码)医院不良事件 各类事件分析、分类、处理流程

医疗安全不容忽视&#xff01; 医疗安全&#xff08;不良&#xff09;事件是指在临床诊疗活动中以及医院运行过程中&#xff0c;任何可能影响患者的诊疗结果、增加患者的痛苦和负担并可能引发医疗纠纷或医疗事故&#xff0c;以及影响医疗工作的正常运行和医务人员人身安全的因…

我给KTV服务生讲解防抖,他竟然听懂了

端午节三天假期&#xff0c;的最后一天&#xff0c;我和朋友闲来无事&#xff0c;想着去唱会儿歌吧&#xff0c;好久不唱了&#xff0c;于是吃了午饭&#xff0c;石景山就近找了一家KTV&#xff0c;我们团好了卷就过去了。 装修还算不错&#xff0c;很快找到服务生&#xff0c…

【创建SpringBoot项目常见问题】保姆级教程(踩过的坑)

文章目录 特别提醒无效目标发行版 18类文件具有错误的版本 61.0, 应为 52.0Spring 项目运行,控制台乱码Spring 配置文件乱码引入插件&#xff0c;idea找不到 在创建第一个SpringBoot项目时&#xff0c;我出现了很多的配置错误&#xff0c;接下来与大家分享一下解决方法。希望我…

讯方技术与华为终端签署鸿蒙合作协议,将为企业助培百万鸿蒙人才

1月18日&#xff0c;鸿蒙生态千帆启航仪式在深圳举行&#xff0c;华为宣布HarmonyOS NEXT鸿蒙星河版开发者预览面向开发者开放申请&#xff0c;这意味着鸿蒙生态进入第二阶段&#xff0c;将加速千行百业的应用鸿蒙化。讯方技术总裁刘国锋、副总经理刘铭皓应邀出席启航仪式&…

基于esp8266_点灯blinker_智能家居

文章目录 一 实现思路1 项目简介2 项目构成3 代码实现4 外壳部分 二 效果展示UI图片 一 实现思路 摘要&#xff1a;esp8266&#xff0c;mixly&#xff0c;点灯blinker&#xff0c;物联网&#xff0c;智能家居&#xff0c;3donecut 1 项目简介 1 项目效果 通过手机blinker app…

17- Redis 中的 quicklist 数据结构

在 Redis 3.0 之前&#xff0c;List 对象的底层数据结构是双向链表或者压缩列表&#xff0c;然后在 Redis 3.2 的时候&#xff0c;List 对象的底层改由 quicklist 数据结构实现。 其实 quicklist 就是【双向链表 压缩列表】组合&#xff0c;因为一个 quicklist 就是一个链表&…

解锁 DevOps 精通:成功的综合指南

在动态的软件开发领域&#xff0c;要掌握 DevOps&#xff0c;需要对其核心原则有细致的了解&#xff0c;并采取战略性实施方法。DevOps 是一种协作方法&#xff0c;它将软件开发 (Dev) 和 IT 运营 (Ops) 结合起来&#xff0c;以自动化和简化软件交付流程。它旨在缩短开发周期、…

双模蓝牙芯片TD5165A功能介绍—拓达半导体

拓达芯片TD5165A是一颗支持U盘&TF卡的双模蓝牙芯片&#xff0c;此颗芯片的亮点在于同时支持音频蓝牙与BLE数传&#xff0c;芯片在支持蓝牙无损音乐播放的同时&#xff0c;还支持 APP和小程序&#xff0c;通过BLE通道对芯片进行控制&#xff0c;同时也支持通过蓝牙串口透传数…

抖动的评估(TJ 和 TIE 的关系)

TIE&#xff1a;时间间隔误差(Time Interval Error,简称TIE)抖动&#xff0c;即在很长的一串波形中&#xff0c;每次边缘的位置相对理想clk 的抖动。 TJBER &#xff1a;TJ&#xff08;Total Jitter&#xff09;总体抖动&#xff0c;为某误码率&#xff08;Bit Error Ratio&am…

网络流常用示意图及基本概念

【网络流简介】 ● 网络流基本概念网络&#xff1a;网络是一个有向有权图&#xff0c;包含一个源点和一个汇点&#xff0c;没有反平行边。网络流&#xff1a;是定义在网络边集上的一个非负函数&#xff0c;表示边上的流量。网络最大流&#xff1a;在满足容量约束和流量守恒的前…

..\USER\stm32f10x.h(298): error: #67: expected a “}“

原keil4的示例工程在用keil5打开之后出现报错&#xff1a; ..\USER\stm32f10x.h(298): error: #67: expected a "}" 在去掉手动添加的一个宏定义STM32F10X_HD后即可正常编译&#xff0c;因为KEIL5已经自动添加了

VR 大厦巡检机器人:开启智能化巡检新时代

在现代城市的高楼大厦中&#xff0c;保障建筑物的安全和功能正常运作是至关重要的。随着建筑结构日益复杂&#xff0c;隐蔽角落和繁杂管道线路的存在使得传统人工巡检面临诸多挑战和局限。电路老化、狭窄通道、拐角等潜在安全隐患&#xff0c;往往难以通过人工巡检完全覆盖&…

【STM32HAL库学习】定时器功能、时钟以及各种模式理解

一、文章目的 记录自己从学习了定时器理论->代码实现使用定时->查询数据手册&#xff0c;加深了对定时器的理解以及该过程遇到了的一些不清楚的知识。 上图为参考手册里通用定时器框图&#xff0c;关于定时器各种情况的工作都在上面了&#xff0c;在理论学习和实际应用后…

spring常用注解(八)@Async

一、介绍 1、介绍 二、原理 三、集成与使用 1、集成方法 &#xff08;1&#xff09;开启 使用以下注解开启 EnableAsync &#xff08;2&#xff09;使用 在需要异步处理的方法上加上 Async 2、返回值 Async注解的方法返回值只能为void或者Future<T>。 &…

轻松实现App推广代理结算,Xinstall超级渠道功能助您一臂之力!

在App推广的广阔天地中&#xff0c;与渠道方建立合作关系&#xff0c;共同实现用户增长和品牌提升&#xff0c;已成为众多开发者和广告主的共识。然而&#xff0c;如何高效管理这些渠道、监测推广效果、实现代理结算&#xff0c;一直是困扰大家的难题。今天&#xff0c;我们就来…

比较器 XD393 XINLUDA(信路达) DIP-8 2.5mA 模拟比较器 双路差动

XD393是一款比较器集成电路&#xff0c;适用于各种电子设备中的信号比较和处理。它的应用领域可能包括但不限于以下几个方面&#xff1a; 1. 电源管理&#xff1a;在电源管理系统中&#xff0c;XD393可以用来监控电压水平&#xff0c;确保系统稳定运行&#xff0c;或者触发某…