解决在Android中使用SecretKey生物验证后仍提示android.security.KeyStoreException: Key user not authenticated的问题

原标题:解决在安卓中使用KeyStore和SecretKey使用安全硬件对数据加密解密时明明已请求生物验证,但仍报错:android.security.KeyStoreException: Key user not authenticated的问题

一、问题代码

这是我生成密钥和获取SecretKey 工具类,我的想法时在用户通过身份验证之后获取SecretKey,将SecretKey 存放到本地变量里面,后续就可以继续使用了。

但是这种做法就会抛出我上面提到的那个问题,我原本的排查思路在想是不是我生成的位置不对,到底放在请求生物认证之前还是之后,于是各种办法都试了还问了ChatGPT也没解决掉。

public class CipherHelper {

    private static SecretKey cachedSecretKey = null;

    /**
     * 在设备的 Android 密钥库(AndroidKeyStore)中生成一个随机的 AES 密钥。
     * 这个密钥可以用于加密和解密数据,并且该密钥受设备的生物识别(如指纹)保护。
     * 这意味着每次要使用这个密钥时,都需要通过生物识别。
     */
    @RequiresApi(api = Build.VERSION_CODES.R)
    public static void generateSecretKey() {
        try {
            KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
            keyStore.load(null);

            KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
            keyGenerator.init(new KeyGenParameterSpec.Builder(
                    CIPHER_KEYSTORE_ALIAS,
                    KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                    .setUserAuthenticationRequired(true)
                    .build());

            cachedSecretKey = keyGenerator.generateKey();
        } catch (Exception e) {
            throw new RuntimeException("无法生成密钥。");
        }
    }


    public static SecretKey getSecretKey() {
        if (cachedSecretKey == null) {
            try {
                KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
                keyStore.load(null);
                cachedSecretKey = (SecretKey) keyStore.getKey(CIPHER_KEYSTORE_ALIAS, null);
            } catch (Exception e) {
                throw new RuntimeException("无法获取密钥", e);
            }
        }
        return cachedSecretKey;
    }

}

Activity的代码:其中的onCipherStrategyCreated方法是加密策略的选择

/**
     * 使用指纹验证进行身份验证
     */
    private void authenticateWithFingerprint() {
        BiometricPrompt biometricPrompt = new BiometricPrompt(this,
                ContextCompat.getMainExecutor(this),
                new BiometricPrompt.AuthenticationCallback() {
                    @Override
                    public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
                        super.onAuthenticationError(errorCode, errString);
                        Toast.makeText(getApplicationContext(), "指纹认证错误: " + errString, Toast.LENGTH_SHORT).show();
                        startAppropriateFlow();
                    }

                    @Override
                    public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
                        super.onAuthenticationSucceeded(result);
                        CipherHelper.generateSecretKey();
                        Toast.makeText(getApplicationContext(), "指纹认证成功", Toast.LENGTH_SHORT).show();
                        onCipherStrategyCreated(new FingerprintCipherStrategy(), ENCRYPTION_TYPE_FINGERPRINT);
                    }

                    @Override
                    public void onAuthenticationFailed() {
                        super.onAuthenticationFailed();
                        Toast.makeText(getApplicationContext(), "指纹认证失败", Toast.LENGTH_SHORT).show();
                        startAppropriateFlow();
                    }
                });

        BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
                .setTitle(getString(R.string.fingerprint_title))
                .setSubtitle(getString(R.string.fingerprint_subtitle))
                .setNegativeButtonText(getString(R.string.cancel))
                .build();

        biometricPrompt.authenticate(promptInfo);
    }

二、问题原因

然后就没办法了呗,只能去查看官方的文档,毕竟Google和百度都没有好的结果,结果我看到了这样一段话:
文档地址

它的意思是我们通过密钥库,实际上的加密和解密操作是送到执行加密操作的系统进程进行操作的,并没有在我们内存里面。
在这里插入图片描述

然后往下翻还有一段话,看到这段话我基本就懂了,使用生物识别的时候加解密应该就是有时效性(或者次数性)的,因为它给到我们一个方法允许我们设置一个指定秒数,在这个指定秒数里面我们可以自由的使用SecretKey加解密,于是我们修改代码
在这里插入图片描述

public class CipherHelper {

    private static SecretKey cachedSecretKey = null;

    /**
     * 在设备的 Android 密钥库(AndroidKeyStore)中生成一个随机的 AES 密钥。
     * 这个密钥可以用于加密和解密数据,并且该密钥受设备的生物识别(如指纹)保护。
     * 这意味着每次要使用这个密钥时,都需要通过生物识别。
     */
    @RequiresApi(api = Build.VERSION_CODES.R)
    public static void generateSecretKey() {
        try {
            KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
            keyStore.load(null);

            KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
            keyGenerator.init(new KeyGenParameterSpec.Builder(
                    CIPHER_KEYSTORE_ALIAS,
                    KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                    .setUserAuthenticationRequired(true)
                    .setInvalidatedByBiometricEnrollment(false)
                            .setUserAuthenticationParameters(60, KeyProperties.AUTH_BIOMETRIC_STRONG)
                    .build());

            cachedSecretKey = keyGenerator.generateKey();
        } catch (Exception e) {
            throw new RuntimeException("无法生成密钥。");
        }
    }


    public static SecretKey getSecretKey() {
        if (cachedSecretKey == null) {
            try {
                KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
                keyStore.load(null);
                cachedSecretKey = (SecretKey) keyStore.getKey(CIPHER_KEYSTORE_ALIAS, null);
            } catch (Exception e) {
                throw new RuntimeException("授权已过期或无法获取密钥", e);
            }
        }
        return cachedSecretKey;
    }

这样设置个参数就解决了异常问题,因为我这个App是没有账号体系的,只使用密钥认证,所以我同时设置了setInvalidatedByBiometricEnrollment在生物认证方式发生变化时使密钥仍然生效,这个默认是true。

然后使用setUserAuthenticationParameters设置有效期60秒,在60秒之后就仍需认证了,这样就解决了我的问题。

版权所有:XuanRan
未经书面授权,禁止转账

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是微信小程序使用Spring Security和JWT实现权限验证的Java代码示例: 1. 配置Spring Security 创建一个SecurityConfig类,配置Spring Security的基本设置和JWT过滤器: ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowired private JwtAuthorizationTokenFilter jwtAuthorizationTokenFilter; @Autowired private UserDetailsService userDetailsService; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.csrf().disable() .authorizeRequests() .antMatchers("/api/auth/**").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint) .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); httpSecurity.addFilterBefore(jwtAuthorizationTokenFilter, UsernamePasswordAuthenticationFilter.class); httpSecurity.headers().cacheControl(); } @Override public void configure(WebSecurity webSecurity) throws Exception { webSecurity.ignoring().antMatchers(HttpMethod.POST, "/api/auth/login"); } } ``` 2. 创建JWT工具类 创建一个JwtTokenUtil类,用于处理JWT的生成、解析和验证: ```java @Component public class JwtTokenUtil { private static final Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class); private static final String CLAIM_KEY_USERNAME = "sub"; private static final String CLAIM_KEY_CREATED = "iat"; private static final String CLAIM_KEY_EXPIRED = "exp"; @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; public String getUsernameFromToken(String token) { String username; try { final Claims claims = getClaimsFromToken(token); username = claims.getSubject(); } catch (Exception e) { username = null; } return username; } public Date getCreatedDateFromToken(String token) { Date created; try { final Claims claims = getClaimsFromToken(token); created = new Date((Long) claims.get(CLAIM_KEY_CREATED)); } catch (Exception e) { created = null; } return created; } public Date getExpirationDateFromToken(String token) { Date expiration; try { final Claims claims = getClaimsFromToken(token); expiration = claims.getExpiration(); } catch (Exception e) { expiration = null; } return expiration; } private Claims getClaimsFromToken(String token) { Claims claims; try { claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } catch (Exception e) { claims = null; } return claims; } private Date generateExpirationDate() { return new Date(System.currentTimeMillis() + expiration * 1000); } private Boolean isTokenExpired(String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(new Date()); } public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } private String generateToken(Map<String, Object> claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public Boolean canTokenBeRefreshed(String token) { return !isTokenExpired(token); } public String refreshToken(String token) { String refreshedToken; try { final Claims claims = getClaimsFromToken(token); claims.put(CLAIM_KEY_CREATED, new Date()); refreshedToken = generateToken(claims); } catch (Exception e) { refreshedToken = null; } return refreshedToken; } public Boolean validateToken(String token, UserDetails userDetails) { JwtUser user = (JwtUser) userDetails; final String username = getUsernameFromToken(token); return ( username.equals(user.getUsername()) && !isTokenExpired(token)); } } ``` 3. 创建JWT过滤器 创建一个JwtAuthorizationTokenFilter类,用于处理JWT的解析和验证: ```java @Component public class JwtAuthorizationTokenFilter extends OncePerRequestFilter { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { logger.debug("processing authentication for '{}'", request.getRequestURL()); final String requestHeader = request.getHeader("Authorization"); String username = null; String authToken = null; if (requestHeader != null && requestHeader.startsWith("Bearer ")) { authToken = requestHeader.substring(7); try { username = jwtTokenUtil.getUsernameFromToken(authToken); } catch (IllegalArgumentException e) { logger.error("an error occured during getting username from token", e); } catch (ExpiredJwtException e) { logger.warn("the token is expired and not valid anymore", e); } } else { logger.warn("couldn't find bearer string, will ignore the header"); } logger.debug("checking authentication for user '{}'", username); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); logger.info("authenticated user '{}', setting security context", username); SecurityContextHolder.getContext().setAuthentication(authentication); } } chain.doFilter(request, response); } } ``` 4. 创建JWT用户类 创建一个JwtUser类,实现Spring SecurityUserDetails接口: ```java public class JwtUser implements UserDetails { private final Long id; private final String username; private final String password; private final Collection<? extends GrantedAuthority> authorities; private final boolean enabled; private final Date lastPasswordResetDate; public JwtUser( Long id, String username, String password, Collection<? extends GrantedAuthority> authorities, boolean enabled, Date lastPasswordResetDate ) { this.id = id; this.username = username; this.password = password; this.authorities = authorities; this.enabled = enabled; this.lastPasswordResetDate = lastPasswordResetDate; } @JsonIgnore public Long getId() { return id; } @Override public String getUsername() { return username; } @JsonIgnore @Override public boolean isAccountNonExpired() { return true; } @JsonIgnore @Override public boolean isAccountNonLocked() { return true; } @JsonIgnore @Override public boolean isCredentialsNonExpired() { return true; } @JsonIgnore @Override public String getPassword() { return password; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public boolean isEnabled() { return enabled; } @JsonIgnore public Date getLastPasswordResetDate() { return lastPasswordResetDate; } } ``` 5. 创建登录控制器 创建一个AuthController类,用于处理用户登录和令牌的生成: ```java @RestController @RequestMapping("/api/auth") public class AuthController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private UserDetailsService userDetailsService; @PostMapping("/login") public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtAuthenticationRequest authenticationRequest) throws AuthenticationException { logger.info("authenticating user '{}'", authenticationRequest.getUsername()); final Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( authenticationRequest.getUsername(), authenticationRequest.getPassword() ) ); SecurityContextHolder.getContext().setAuthentication(authentication); final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername()); final String token = jwtTokenUtil.generateToken(userDetails); return ResponseEntity.ok(new JwtAuthenticationResponse(token)); } } ``` 6. 创建异常处理器 创建一个JwtAuthenticationEntryPoint类,用于处理不允许访问的请求: ```java @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { logger.error("unauthorized error: {}", authException.getMessage()); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); } } ``` 7. 创建请求对象 创建一个JwtAuthenticationRequest类,用于保存用户登录请求: ```java public class JwtAuthenticationRequest implements Serializable { private static final long serialVersionUID = -8445943548965154778L; @NotBlank private String username; @NotBlank private String password; public JwtAuthenticationRequest() { super(); } public JwtAuthenticationRequest(String username, String password) { this.setUsername(username); this.setPassword(password); } public String getUsername() { return this.username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return this.password; } public void setPassword(String password) { this.password = password; } } ``` 8. 创建响应对象 创建一个JwtAuthenticationResponse类,用于保存令牌响应: ```java public class JwtAuthenticationResponse implements Serializable { private static final long serialVersionUID = 1250166508152483573L; private final String token; public JwtAuthenticationResponse(String token) { this.token = token; } public String getToken() { return this.token; } } ``` 以上是微信小程序使用Spring Security和JWT实现权限验证的Java代码示例,希望能对你有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值