Security 原理与总结
创始人
2024-09-25 12:49:27
0

认证与授权

认证:系统提供的用于识别用户身份的功能,通常提供用户名和密码进行登录其实就是在进行认证,认证的目的是让系统知道你是谁。(可用于动态菜单显示)

授权:用户认证成功后,需要为用户授权,其实就是指定当前用户可以操作哪些功能。(用户接口调用授权)

一、RBAC权限模式

业界通常基于RBAC实现授权

RBAC(Role-Based Access Control):基于角色的权限访问控制。它的核心在于用户只和角色关联,而角色代表了权限,是一系列权限的集合。RBAC的核心元素包括:用户、角色、权限。

二、Spring Security

一、介绍

Spring Security 的前身是 Acegi Security ,是 Spring 项目组中用来提供安全认证服务的框架。
Spring Security是一个功能强大且高度可定制的安全框架,专为基于Spring的企业应用系统提供声明式的安全访问控制解决方案。使用SrpingSecurity可以帮助我们简化认证和授权的过程。

二、原理

Spring Security的原理主要基于过滤器链。当一个请求到达Spring应用时,它首先会经过一些列的过滤器,这些过滤器负责身份验证、授权以及其他安全相关的任务。

  • CsrfFilter:用于处理跨站请求,防止跨站请求伪造共计,是导致POST请求失败的原因。
  • AuthorizaionFilter:负责授权模块。
  • UsernamePasswordAuthenticationFilter用于处理基于表单的请求登录。

这些Filter构成了Spring Security的核心功能,通过它们,可以实现身份验证、授权。防护等。

三、使用

1.依赖配置

                       org.springframework.boot           spring-boot-starter-security                                       io.jsonwebtoken           jjwt           0.9.1              

2.配置类

可以设置密码加密密码加密方式,自定义异常处理器(401 未认证;403 无权限),如使用BCrypt加密方式替换底层加密方式,设置自定义的的安全认证,关闭CSRF防止请求被拦截等。

Security自带BCrypt加密工具类BCryptPasswordEncoder,每次加密使用的salt盐都不同,因此得到的加密后密码哈希值都不通过,但是可以使用BCryptPasswordEncoder中的matches()进行密码比较。

package com.example.boot.security;  import com.example.boot.security.sms.SmsCodeAuthenticationProvider; import com.example.interceptor.JwtAuthenticationTokenFilter; import com.example.service.Impl.SpringSecurityDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;  /**  * 权限认证配置类  *  * @author ding  * @since 2024/7/23  */ @Configuration @EnableWebSecurity //开启注解权限认证功能    jsr250Enabled = true   securedEnabled = true @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter {      //放行资源  通过split进行分割     public static final String STATIC_PATH = "";      //账号密码登录认证过滤器     @Autowired     private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;      @Autowired     private SpringSecurityDetailsService securityDetailsService;      // 认证失败处理器     @Autowired     private AuthenticationEntryPoint authenticationEntryPoint;      //授权失败处理器     @Autowired     private AccessDeniedHandler accessDeniedHandler;      //密码处理器     @Bean     public PasswordEncoder passwordEncoder() {         return new BCryptPasswordEncoder();     }       /**      * 认证配置      */     @Override     protected void configure(HttpSecurity http) throws Exception {         http             //关闭CSRF(跨服务器的请求访问),默认是开启的,设置为禁用;如果使用自定义登录页面需要关闭此项,否则登录操作会被禁用403                 .csrf().disable()                 // 禁用session (前后端分离项目,不通过Session获取SecurityContext)                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)                 .and()                 //请求认证配置                 .authorizeRequests()                 .antMatchers("/login", "/findUserName", "/socket/**",                         "/getPermCode", "/swagger-ui.html",  "/webjars/**", "/swagger-resources/**", "/v2/**",                         "/swagger-ui.html", "/doc.html", "/files/**",                         "/register", "/sendSms/**", "/validSms").permitAll()                 .anyRequest().authenticated();          //添加token过滤器 //        http.addFilterBefore( smsCodeCheckFilter, UsernamePasswordAuthenticationFilter.class);         http.addFilterBefore( jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);          // 配置异常处理器         http.exceptionHandling()                 //认证失败                 .authenticationEntryPoint(authenticationEntryPoint)                 //授权失败                 .accessDeniedHandler(accessDeniedHandler);          // 退出登录处理器 清除redis 中token GET请求 //        http.logout().logoutUrl("/admin/logout");          //Spring Security 允许跨域         http.cors();     }      @Override     protected void configure(AuthenticationManagerBuilder auth) throws Exception {         // 加入自定义的安全认证  如果自定义了多套身份验证系统  需要在这添加重写的 AuthenticationProvider         auth.userDetailsService(this.securityDetailsService)                 .passwordEncoder(this.passwordEncoder())                 .and()                 //添加自定义的认证管理类                 .authenticationProvider(smsAuthenticationProvider())                 .authenticationProvider(authenticationProvider());     }      //注入AuthenticationManager 进行用户认证     @Bean     @Override     protected AuthenticationManager authenticationManager() throws Exception {         return super.authenticationManager();     }      //账号密码     @Bean     public AuthenticationProvider authenticationProvider(){         AuthenticationProvider authenticationProvider =  new UsernameAuthenticationProvider();         return authenticationProvider;     }          //手机验证码     @Bean     public AuthenticationProvider smsAuthenticationProvider(){         AuthenticationProvider authenticationProvider =  new SmsCodeAuthenticationProvider();         return authenticationProvider;     } } 

3.用户登录接口

账号密码登录方式

    public Admin login(Admin admin) {          UsernameAuthenticationToken authenticationToken = new UsernameAuthenticationToken(admin.getUsername(), admin.getPassword());         Authentication authenticate = authenticationManager.authenticate(authenticationToken);         if (ObjectUtil.isNull(authenticate)) {             throw new CustomException(ResultCodeEnum.USER_ACCOUNT_ERROR);         }          //认证通过 生成token         LoginUser loginUser = (LoginUser) authenticate.getPrincipal();         Integer userId = loginUser.getAdmin().getId();         Map claims = new HashMap<>();         claims.put(JwtClaimsConstant.ADMIN_ID, userId);         claims.put(JwtClaimsConstant.USERNAME, loginUser.getUsername());         String token = JwtUtil.createJWT(                 jwtProperties.getAdminSecretKey(),                 jwtProperties.getAdminTtl(),                 claims         );          Admin dbAdmin = loginUser.getAdmin();         dbAdmin.setToken(token);          //认证通过 存入redis         redisTemplate.opsForValue().set(KeyEnum.TOKEN + "_" + userId, JSONUtil.toJsonStr(loginUser.getAdmin()));         redisTemplate.expire(KeyEnum.TOKEN + "_" + userId, 4, TimeUnit.HOURS);         return dbAdmin;     }

 4.自定义Dao层方法loadUserByUsername

可以从数据库中爬取对应数据

package com.example.service.Impl;  import cn.hutool.core.util.ObjectUtil; import com.example.mapper.AdminMapper; import com.example.pojo.entity.*; import com.example.pojo.utils.LoginUser; import com.github.yulichang.wrapper.MPJLambdaWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils;  /**  * TODO  *  * @author ding  * @since 2024/7/23  */ @Service public class SpringSecurityDetailsService implements UserDetailsService {     @Autowired     private AdminMapper adminMapper;      @Override     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {          MPJLambdaWrapper mpjLambdaWrapper = new MPJLambdaWrapper<>();         mpjLambdaWrapper                 .selectAll(Admin.class)                 .selectCollection(Role.class, Admin::getRoles,set -> set.collection(Permission.class, Role::getPermissions))                 .innerJoin(UserAndRole.class, UserAndRole::getUserId, Admin::getId)                 .innerJoin(Role.class, Role::getId, UserAndRole::getRoleId)                 .innerJoin(RoleAndPermission.class, RoleAndPermission::getRoleId, Role::getId)                 .innerJoin(Permission.class, Permission::getId, RoleAndPermission::getPermissionId)                 .eq(StringUtils.hasLength(username), Admin::getUsername, username);         Admin admin = adminMapper.selectJoinOne(Admin.class, mpjLambdaWrapper);         if (ObjectUtil.isNull(admin)) {             throw new RuntimeException("当前用户不存在");         }          //封装LoginUser对象 包含账户名 密码 是否可用         LoginUser loginUser = new LoginUser(admin);          //将Authentication对象(用户信息,已认证转态、权限信息) 存入 SecurityContextConfig         UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());         SecurityContextHolder.getContext().setAuthentication(authenticationToken);         return loginUser;     } } 

定义后会替换到底层中的方法,在DaoAuthenticationProvider中,如果我们自定义该方法,就会跳转到该方法。

如果使用自定义的认证方法,我们需要再自定义的 短信登录身份认证组件 中写入该方法

package com.example.boot.security.sms;  /**  * TODO  *  * @author ding  * @since 2024/7/24  */  import com.example.mapper.AdminMapper; import com.example.pojo.entity.*; import com.example.pojo.utils.LoginUser; import com.github.yulichang.wrapper.MPJLambdaWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils;   /**  * @description: 短信登录身份认证组件  **/ @Slf4j @Component public class SmsCodeAuthenticationProvider implements AuthenticationProvider {      @Autowired     private AdminMapper adminMapper;       private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();      @Override     public Authentication authenticate(Authentication authentication) throws AuthenticationException {         String mobile = (String) authentication.getPrincipal();         //根据手机号加载用户         UserDetails user = loadUserByPhone(mobile);          Object principalToReturn = user;         return createSuccessAuthentication(principalToReturn, authentication, user);     }      @Override     public boolean supports(Class aClass) {         //如果是SmsCodeAuthenticationToken该类型,则在该处理器做登录校验         return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass);     }       /**      * 跟账号登录保持一致      * @param principal      * @param authentication      * @param user      * @return      */     protected Authentication createSuccessAuthentication(Object principal,                                                          Authentication authentication, UserDetails user) {         // Ensure we return the original credentials the user supplied,         // so subsequent attempts are successful even with encoded passwords.         // Also ensure we return the original getDetails(), so that future         // authentication events after cache expiry contain the details         UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(                 principal, authentication.getCredentials(),                 authoritiesMapper.mapAuthorities(user.getAuthorities()));         result.setDetails(authentication.getDetails());          return result;     }      /**      * 获取用户信息      * @param phone      * @return      * @throws UsernameNotFoundException      */     public UserDetails loadUserByPhone(String phone) throws UsernameNotFoundException {         MPJLambdaWrapper mpjLambdaWrapper = new MPJLambdaWrapper<>();         mpjLambdaWrapper                 .selectAll(Admin.class)                 .selectCollection(Role.class, Admin::getRoles, set -> set.collection(Permission.class, Role::getPermissions))                 .innerJoin(UserAndRole.class, UserAndRole::getUserId, Admin::getId)                 .innerJoin(Role.class, Role::getId, UserAndRole::getRoleId)                 .innerJoin(RoleAndPermission.class, RoleAndPermission::getRoleId, Role::getId)                 .innerJoin(Permission.class, Permission::getId, RoleAndPermission::getPermissionId)                 .eq(StringUtils.hasLength(phone), Admin::getPhone, phone);         Admin admin = adminMapper.selectJoinOne(Admin.class, mpjLambdaWrapper);         if (admin == null) {             throw new UsernameNotFoundException("该手机号不存在");         }          //封装LoginUser对象 包含账户名 密码 是否可用         LoginUser loginUser = new LoginUser(admin);          //将Authentication对象(用户信息,已认证转态、权限信息) 存入 SecurityContextConfig         UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());         SecurityContextHolder.getContext().setAuthentication(authenticationToken);         return loginUser;     } } 

5.LoginUser实体类

用户封装UserDetail类,UserDetailsService 加载好用户认证信息后会封装认证信息到一个 UserDetails 的实现类。跟方便将相关数据写入redis中,也更容易从redis中取出数据。

package com.example.pojo.utils;  import cn.hutool.core.util.ObjectUtil; import com.example.pojo.entity.Admin; import com.example.pojo.entity.Permission; import com.example.pojo.entity.Role; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails;  import java.util.*;  /**  * TODO  *  * @author ding  * @since 2024/7/23  */ @Data @NoArgsConstructor @AllArgsConstructor public class LoginUser implements UserDetails {      //用户信息     private Admin admin;      public LoginUser(Admin admin) {         this.admin = admin;     }      private List authorities;      /**      * 获取权限信息      */     @Override     public Collection getAuthorities() {         if (!ObjectUtil.isEmpty(authorities)){             return authorities;         }          //将权限信息封装成SimpleGrantedAuthority对象         authorities = new ArrayList<>();          Set roles = admin.getRoles();         roles.forEach(role -> {             //授权角色信息             authorities.add(new SimpleGrantedAuthority(role.getRoleValue()));             Set permissions = role.getPermissions();             permissions.forEach(permission -> {                 //授权权限信息                 authorities.add(new SimpleGrantedAuthority(permission.getKeyword()));             });         });          return authorities;     }      @Override     public String getPassword() {         return admin.getPassword();     }      @Override     public String getUsername() {         return admin.getUsername();     }      /**      * 判断是否过期      */     @Override     public boolean isAccountNonExpired() {         return true;     }      /**      * 是否锁定      */     @Override     public boolean isAccountNonLocked() {         return true;     }      /**      * 是否没有超时      */     @Override     public boolean isCredentialsNonExpired() {         return true;     }      @Override     public boolean isEnabled() {         return admin.getStatus() == 1;     } } 

6.SecurityHandler类

编写授权失败和认证失败的处理器。如果该类没有发挥功能建议看一下自己定义的全局异常处理器是否将异常取出并拦截。

package com.example.boot.security;  import cn.hutool.json.JSONUtil; import com.example.common.Result; import com.example.utils.WebUtils; import org.springframework.context.annotation.Bean; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component;  import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException;  /**  * 授权失败和认证失败的处理器  *  * @author ding  * @since 2024/7/23  */ @Component public class SecurityHandler {      //授权失败     @Bean     public AccessDeniedHandler accessDeniedHandler() {         return (request, response, accessDeniedException) -> {             Result result = Result.error("403","您的权限不足");             String json = JSONUtil.toJsonStr(result);             //将字符串渲染到客户端             WebUtils.renderString403(response, json);         };     }      //认证失败     @Bean     public AuthenticationEntryPoint authenticationEntryPoint() {         return (request, response, authException) -> {             Result result = Result.error("401", "认证失败,请重新登录");             String json = JSONUtil.toJsonStr(result);             WebUtils.renderString401(response, json);         };     }  } 

7.创建JWT认证过滤器

该方法会检查HTTP中的URL路径,验证JWT令牌,并在验证成功后将用户信息放入Spring Security的上下文中。若为登录方法,则直接放心。

package com.example.interceptor;  import cn.hutool.json.JSONUtil; import com.example.common.enums.KeyEnum; import com.example.common.enums.ResultCodeEnum; import com.example.common.exception.CustomException; import com.example.constant.JwtClaimsConstant; import com.example.context.BaseContext; import com.example.pojo.entity.Admin; import com.example.pojo.utils.LoginUser; import com.example.properties.JwtProperties; import com.example.utils.IpUtils; import com.example.utils.JwtUtil; import io.jsonwebtoken.Claims; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter;  import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Objects;  /**  * jwt令牌校验的拦截器  *  * @author ding  * @since 2024/7/23  */ @Component @Slf4j public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {      @Autowired     private JwtProperties jwtProperties;      @Autowired     private RedisTemplate redisTemplate;      @Override     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {         if (request.getRequestURI().equals("/login") || request.getRequestURI().equals("/validSms")){             //调用登录方法             String ip = IpUtils.getIp();             //UV中新增相关数据             redisTemplate.opsForHyperLogLog().add(KeyEnum.UV, ip);             //当前在线人数中新增数据             redisTemplate.opsForSet().add(KeyEnum.ONLINE, ip);              filterChain.doFilter(request, response);             return;         }          if (request.getRequestURI().equals("/logOut")){             //如果调用退出方法,说明当前用户退出了系统             String ip = IpUtils.getIp();             //将该ip数据从redis中删除             redisTemplate.opsForSet().remove(KeyEnum.ONLINE, ip);         }          //从请求头中获取token         String token = request.getHeader("token");         //没有token         if (!StringUtils.hasLength(token)){             //放行,因为后面的会抛出响应的异常 401 403             filterChain.doFilter(request, response);             return;         }          //有token         Integer userId;         try {             log.info("jwt校验:{}", token);             Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);             Long adminId = Long.valueOf(claims.get(JwtClaimsConstant.ADMIN_ID).toString());             //在线程中设置当前登录的ID             BaseContext.setCurrentId(adminId);              userId = Math.toIntExact(adminId);         }catch (Exception e){             e.printStackTrace();             throw new CustomException(ResultCodeEnum.TOKEN_INVALID_ERROR);         }          //从redis中获取用户信息         String redisKey = KeyEnum.TOKEN + "_" + userId;         Object obj = redisTemplate.opsForValue().get(redisKey);         if (Objects.isNull(obj)){             throw new CustomException(ResultCodeEnum.TOKEN_CHECK_ERROR);         }          Admin admin = JSONUtil.toBean(obj.toString(), Admin.class);         LoginUser loginUser = new LoginUser(admin);         //将Authentication对象(用户信息、已认证状态、权限信息)存入SecurityContextHolder         UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());         SecurityContextHolder.getContext().setAuthentication(authenticationToken);          //放行         filterChain.doFilter(request, response);     } } 

8.使用

在配置类上加注解@EnableGlobalMethodSecurity(prePostEnabled = true),即可开启注解验证是否授权的功能。

/**  * 权限认证配置类  *  * @author ding  * @since 2024/7/23  */ @Configuration @EnableWebSecurity //开启注解权限认证功能    jsr250Enabled = true   securedEnabled = true @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter {        ... }

根据当前登录用户绑定的角色是否拥有 PROJECT_QUERY 权限,判断当前登录用户是否具有权限。 

     @ApiOperation("通过项目Id获取项目信息")     @GetMapping("/selectById/{id}")     @PreAuthorize("hasAuthority('PROJECT_QUERY')")     public Result selectById(@ApiParam("项目id") @PathVariable Integer id){         Project project = projectService.selectById(id);         return Result.success(project);     }

相关内容

热门资讯

苹果6是安卓系统,跨界融合的科... 你有没有想过,苹果6竟然是安卓系统?没错,你没听错,就是那个以封闭著称的苹果,竟然和安卓系统有着千丝...
安卓系统同意使用相机,开启便捷... 手机里的相机功能是不是让你爱不释手?不过,你知道吗?在使用安卓系统手机的时候,每次打开相机,其实都是...
安卓系统系统安全性,安卓系统安... 你知道吗?在智能手机的世界里,安卓系统和苹果系统就像是一对双胞胎,各有各的个性,各有各的“小秘密”。...
安卓系统安装手柄驱动,轻松连接... 你有没有想过,你的安卓手机也能摇身一变,成为游戏玩家的天堂?没错,就是那种拿着手柄,操作自如的感觉,...
飞车iOS系统转安卓,跨平台角... 你有没有想过,有一天你的QQ飞车角色从苹果手机跳到安卓手机,那会是怎样的场景呢?想象你的角色在两个世...
怎么消除安卓系统定位,保护个人... 亲爱的手机控们,你们有没有遇到过这样的情况:手机里的GPS定位功能突然变得超级“热心”,不管你走到哪...
安卓系统高版本游戏,体验安卓高... 你有没有发现,随着安卓系统的不断升级,越来越多的游戏开始支持高版本系统了呢?这可真是让人兴奋不已啊!...
手机京东自营安卓系统,安卓系统... 你有没有发现,最近手机圈里可是热闹非凡呢!尤其是京东自营的安卓手机,简直让人眼花缭乱。今天,就让我带...
不是基于安卓的系统,非安卓系统... 最近科技圈可是炸开了锅!谷歌宣布停止开源安卓系统,这消息一出,可真是让无数开发者们心惊肉跳。不过,别...
象棋大师安卓系统2.0,指尖上... 你知道吗?最近我在手机上发现了一个超级酷的象棋APP,它就是象棋大师安卓系统2.0版!这款APP简直...
如何把安卓系统变成ios系统,... 你有没有想过,把你的安卓手机变成苹果手机呢?想象那光滑的界面、流畅的操作,还有那独特的iOS体验,是...
安卓转苹果系统使用,王者荣耀玩... 你终于下定决心要抛弃安卓,拥抱苹果的温暖怀抱了?别急,别急,让我这个过来人给你好好捋一捋,从安卓到苹...
安卓系统显示doc文档,安卓系... 手机里的文档怎么打不开啦?别急,让我来告诉你安卓系统显示doc文档的绝招!安卓系统显示doc文档,你...
电脑安卓系统安装器,轻松体验安... 你有没有想过,在电脑上也能畅玩安卓游戏,看安卓电影呢?没错,就是那种你在手机上爱不释手的安卓系统,现...
安卓 系统电量图标隐藏,个性化... 手机屏幕上那些密密麻麻的图标,是不是让你眼花缭乱?别急,今天就来教你一招,轻松隐藏安卓系统电量图标,...
安卓系统设置字体粗细,安卓系统... 你有没有发现,手机上的文字有时候看起来有点小,有点累眼呢?别急,今天就来教你怎么轻松调整安卓系统里的...
安卓系统书 知乎,从知乎视角探... 你有没有想过,你的手机里那个神奇的安卓系统,是怎么一步步从一个小小的种子,长成如今覆盖全球的参天大树...
安卓系统照片设墙纸,照片一键设... 亲爱的手机控们,你是否曾为手机壁纸的设置而烦恼?想要把那些美美的照片变成手机背景,却总是遇到各种小麻...
安卓系统怎样打印文档,轻松实现... 你有没有想过,手机里的那些文档,怎么就能变成实实在在的纸张呢?没错,就是通过安卓系统打印出来!今天,...
能把安卓系统换成ios系统吗,... 你有没有想过,把你的安卓手机变成苹果手机,是不是就像变魔术一样神奇呢?想象你的手机屏幕上突然出现了苹...