草庐IT

Spring Security + JWT + Swagger2 登录验证一套流程小结

抽象带狮 2023-03-28 原文

Spring Security + JWT + Swagger2 登录验证一套流程

主要是三个框架的集成配置,以及各个独立的配置(主要是 JWT + Security 的登录验证)。

流程:

  • 构建 Spring Boot 基本项目,准备数据库表 User —— 用于存放登录实体类信息。
  • 配置 Security 和 Swagger2 环境,确保没有什么问题。
  • 构建 RespBean——公共返回实体类JwtTokenUtil——JWT token 工具类User——登录实体类
  • 让 User 实现 UserDetails 接口,重写部分方法。
  • 配置 Security 实现重写 UserDetailsService 方法,以及 PasswordEncoder——密码凭证器 并加上 @Bean 注解。这两个主要用于设置 Security 的认证。
  • 构建 jwtAuthenticationTokenFilter 类——自定义 JWT Token 拦截器,并在 SecurityConfig 的授权方法中添加此拦截器。
  • Swagger2Config 配置类中,配置有关 Security 的 Token 认证。
  • 启动项目查看代码是否准确。

1. 构建 Spring Boot 基本项目,准备数据库——User

项目子模块:authority-security,父模块已引入 Spring boot 依赖 2.3.0

1.1 导入依赖

<dependencies>
    <!-- web 依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- lombok 依赖 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

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

    <!-- mybatis-plus 依赖 -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.3.1.tmp</version>
    </dependency>

    <!-- swagger2 依赖 -->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.7.0</version>
    </dependency>

    <!-- swagger 第三方 UI 依赖 -->
    <dependency>
        <groupId>com.github.xiaoymin</groupId>
        <artifactId>swagger-bootstrap-ui</artifactId>
        <version>1.9.6</version>
    </dependency>

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

    <!-- JWT 依赖 -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>

    <!-- commons-pool2 对象池依赖 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
</dependencies>

构建数据库表:user

create table user(
	id int primary key auto_increment,
	username varchar not null,
	password varchar not null,
	info varchar(200),
	enabled tinyint(1) default 1
)

insert into user values(default,"admin","$2a$10$Himwt.wu3MPOLnNQ9YUH8O2quxgi7bMuomiNeFsVKRay87.qG5dgy","管理员 info ...",default)

username:admin;password:123

配置 application.yml 文件参数:

server:
  port: 8082

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/dbtest16?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: admin
    password: admin
    hikari:
      # 连接池名字
      pool-name: DateHikari
      # 最小空闲连接数
      minimum-idle: 5
      # 空闲连接存活最大事件,默认10分钟(600000)
      idle-timeout: 180000
      # 最大连接数:默认 10
      maximum-pool-size: 10
      # 从连接池返回的连接自动提交
      auto-commit: true
      # 连接最大存活时间,0 表示永久存活,默认 1800000(30 min)
      max-lifetime: 1800000
      # 连接超时事件 30 s
      connection-timeout: 30000
      # 测试连接是否可用的查询语句
      connection-test-query: SELECT 1

# MP 配置
mybatis-plus:
  # 配置 Mapper 映射文件
  mapper-locations: classpath*:/mapper/*Mapper.xml
  # 实体类的别名包
  type-aliases-package: com.cnda.pojo
  configuration:
    # 自动驼峰命名
    map-underscore-to-camel-case: false

# MyBatis 的 SQL 打印是方法接口所在的包
logging:
  level:
    com.cnda.mapper: debug

# JWT 配置
jwt:
  # JWT 存储的请求头
  tokenHeader: Authorization
  # JWT 加密使用的密钥
  secret: test-cnda-secret
  # JWT 的有效时间 (60*60*24)
  expiration: 604800
  # JWT 负载中拿到开头 规定
  tokenHead: Bearer

User 实体类代码:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;
    private String username;
    private String password;
    private String info;
    private Boolean enabled;
}

2. 配置 Security 和 Swagger2 的配置

先配置好这两个确保没有什么问题,因为重点是 JWT,这两个配置比较简单,当搭配了 JWT 之后,Swagger2 也需要与两者集成一些配置,这个后面再说,现在只配置基本设置。

2.1 配置 SecurityConfig

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(
                "/hello",
            	// 下面是对静态资源以及 swagger2 UI 的放行。
            	"/css/**",
                "/js/**",
                "/img/**",
                "/index.html",
                "favicon.ico",
                "/doc.html",
                "/webjars/**",
                "/swagger-resources/**",
                "/v2/api-docs/**",
                "/ws/**"
        );
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }
}

上面使用 WebSecurity 放行了 /hello 请求,在 LoginController 中。

@RestController
public class LoginController {

    @RequestMapping("/hello")
    public String hello(){
        return "Hello Word!";
    }
}

这意味除了 localhost:8082/hello 会被放行,其他请求都会被 Security 拦截重定向到 /login(这个请求 Security 内部已经实现了包括相关页面)。

2.2 配置 Swagger2Config

@Configuration
@EnableSwagger2
public class Swagger2Config {
    @Bean
    public Docket docket(){
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo()) // 配置 apiInfo
                .select() // 选择那些路径和api会生成document
                .apis(RequestHandlerSelectors.basePackage("com.cnda.controller")) // // 对哪些 api进行监控,RequestHandlerSelectors.basePackage 基于包扫描
                .paths(PathSelectors.any()) // 对所有路径进行监控
                .build();

    }

    private ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("在线接口文档")
                .description("在线接口文档")
                .contact(new Contact("cnda","http://localhost:8082/doc.html","xxx@xxx.com"))
                .build();
    }
}

运行效果:

修改一下 Rustful 风格,并加了一个 /hello1 请求,不放行,打印内容相同。

可以看到 Security 和 Swagger2 基本配置完成。

3. 构建 JWT 工具类、公共响应对象

JWT 工具类主要用于生成 JWT,判断 JWT 是否有效,刷新 JWT 等方法。

公共响应对象——RespBean,返回的都已 JSON 格式返回。

3.1 JwtUtil

@Component
public class JwtUtil {
    // 准备两个存放在荷载的内容
    private static final String CLAIM_KEY_SUB = "sub";
    private static final String CLAIM_KEY_CREATE = "ibt";


    // 提取 application.yml 中 JWT 的参数:
    // 1. expiration Long
    @Value("${jwt.expiration}")
    private Long expiration;

    // 2. secret String
    @Value("${jwt.secret}")
    private String secret; // 密钥

    // 根据用户名构建 token
    public String foundJWT(UserDetails userDetails) {
        String username = userDetails.getUsername();
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_SUB, username);
        claims.put(CLAIM_KEY_CREATE, new Date());
        return foundJWT(claims);
    }

    // 根据荷载 map 构建 token
    private String foundJWT(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(getExpiration()) // 过期时间
                .signWith(SignatureAlgorithm.HS512, secret) // 设置签名算法和密钥
                .compact();
    }

    // 判断 token 是否有效
    public boolean validateToken(String token,UserDetails userDetails){
        // 从 token 中获取 username 与 userDetails 中的username 对比
        String username = getUsernameInToken(token);
        // 判断 username 是否一致以及 token 是否过期
        return username.equals(userDetails.getUsername()) && !isExpired(token);
    }


    // 判断 token 是否过期
    // true 过期 false 没过期
    private boolean isExpired(String token) {
        Date expiration = getClaimsInToken(token).getExpiration();
        return expiration.before(new Date());
    }
    // 从 token 中提取荷载信息
    public Claims getClaimsInToken(String token){
        Claims claims = null;

        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        }catch (Exception e){
            e.printStackTrace();
        }
        return claims;
    }

    // 从 token 中提取用户名信息
    public String getUsernameInToken(String token){
        String username;
        try {
            username = getClaimsInToken(token).getSubject();
        }catch (Exception e){
            username = null;
        }
        return username;
    }
    // token 是否能刷新
    public boolean tokenCanRef(String token){
        return !isExpired(token); // 有效地 token 才能被刷新
    }
    // 刷新 token
    public String refToken(String token){
        Claims claimsInToken = getClaimsInToken(token);
        claimsInToken.put(CLAIM_KEY_CREATE,new Date());
        return foundJWT(claimsInToken);
    }

    // 设置过期时间
    private Date getExpiration() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }
}

3.2 RespBean 公共返回对象

@Data
@AllArgsConstructor
@NoArgsConstructor
public class RespBean {
    private long code;
    private String message;
    private Object obj;

    /**
     * 返回响应结果
     */
    private static RespBean result(long code, String message, Object obj) {
        return new RespBean(code, message, obj);
    }
    /*
       返回成功响应
        */
    public static RespBean success(String message) {
        return result(200, message, null);
    }

    /*
    返回成功响应以及数据体
     */
    public static RespBean success(String message, Object obj) {
        return result(200, message, obj);
    }

    /*
    返回错误响应
     */
    public static RespBean error(String message) {
        return result(500, message, null);
    }
}

4. 让 User 实体类实现 UserDetails 的方法成为 Security 验证的用户核心主体

由于 Security 框架的性质,自定义授权和认证时,一般情况下会自定义 UserDetails。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private String info;
    private Boolean enabled;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() { // 权限角色
        return null;
    }

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

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

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

    @Override
    public boolean isEnabled() { // 这里数据库实现了该字段,直接用即可
        return this.enabled;
    }
}

5. 重写 UserDetailsServer 和 PasswordEncoder

5.1 重写 UserDetailsServer

这个类就只有一个方法:

loadUserByUsername(UserDetails details),该方法用于根据用户名加载用户信息,用作于 Security 的后续认证,同时也可以用一个类去实现该接口,这里为了方便,同时也是 Lambda 表达式。

注意:这里的 UserMapper 没有代码展示了,就一个根据用户名查询用户信息的 SQL。

@Resource
private UserMapper mapper;

@Bean
@Override
protected UserDetailsService userDetailsService() {
    return username -> {
        User user = mapper.find(username);
        if (user!=null){
            return user;
        }
        throw new UsernameNotFoundException("用户名或密码不正确");
    };
}

5.2 PasswordEncoder——密码凭证器

这个类主要用于验证表单提交的密码是否和 重写之后的 UserDetailsServer 得到的 UserDetails 中的加密密码一致。

@Bean
public PasswordEncoder encoder(){
    return new BCryptPasswordEncoder();
}

5.3 配置到 SecurityConfig 的认证中

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService()).passwordEncoder(encoder());
}

6. 配置 JWT 的拦截器

public class JwtTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private UserDetailsService service;


    @Value("${jwt.tokenHeader}")
    private String tokenHeader;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        // 获取请求头中的指定的值
        String headerToken = httpServletRequest.getHeader(tokenHeader);
        // 保证 header中的 token 不为 null,且以指定字串开头——Bearer
        if (headerToken!=null && headerToken.startsWith(tokenHead)){
            // 截取有效 token
            String jwtToken = headerToken.substring(tokenHead.length());
            String username = jwtUtil.getUsernameInToken(jwtToken);
            // 判断 UserDetails 中的用户主体是否为null
            if (username!=null && SecurityContextHolder.getContext().getAuthentication() == null){
                // SecurityContextHolder.getContext().getAuthentication() == null 代表着此时 Security 中没有登录的用户主体
                // 此时可以使用有效地 jwtToken 进行用户认证
                UserDetails userDetails = service.loadUserByUsername(username);
                // 判断 token 是否有效
                if (jwtUtil.validateToken(jwtToken,userDetails)){
                    // 如果有效则使用 token 中的信息进行登录
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null, userDetails.getAuthorities());
                    // 根据请求设置 Details,包含了部分请求信息和主体信息。具体效果不清楚...坑
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
                    // 将 authenticationToken 设置到 SecurityContext 中
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);

                }
            }

        }
        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }
}

6.1 将 JWT 拦截器设置到 SecurityConfig 的授权方法中。

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 由于我们使用的是 JWT 令牌的形式来验证用户,所以可以将 csrf 防御关闭
    // JWT 能有效防止 csrf 攻击,强行使用 csrf 可能导致令牌泄露
    http.csrf()
        .disable()
        // 基于 token,不需要使用 Session 了
        .sessionManagement() // Session 管理
        // 管理 Session 创建策略
        //    ALWAYS, 总是创建HttpSession
        //    NEVER, 只会在需要时创建一个HttpSession
        //    IF_REQUIRED, 不会创建HttpSession,但如果它已经存在,将可以使用HttpSession
        //    STATELESS; 永远不会创建HttpSession,它不会使用HttpSession来获取SecurityContext
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .authorizeRequests() // 授权请求
        // 除了上面的请求,其他所有请求都需要认证
        .anyRequest()
        .authenticated()
        .and()
        // 禁止缓存
        .headers()
        .cacheControl();

    // 自定义拦截器 JWT 过滤器
    http.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); // 将过滤器按照一定顺序加入过滤器链。

}

@Bean
public JwtTokenFilter jwtTokenFilter() {
    return new JwtTokenFilter();
}

7. 完善 LoginController 请求,运行项目。

LoginController

@RestController
public class LoginController {
    @Autowired
    private UserService service;

    @GetMapping("/hello")
    public String hello(){
        return "Hello Word!";
    }
    @GetMapping("/hello1")
    public String hello1(){
        return "Hello1 Word!";
    }

    @PostMapping("/login")
    public RespBean loginUser(@RequestBody User user, HttpServletRequest request){
        return service.login(user.getUsername(),user.getPassword(),request);
    }
}

UserService,使用的时 MVC 模式,所以只展示实现类:

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private JwtUtil jwtUtil;

    @Value("${jwt.tokenHead}")
    private String tokenHead;



    @Override
    public RespBean login(String username, String password, HttpServletRequest request) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if (userDetails==null || !passwordEncoder.matches(password,userDetails.getPassword())){
            return RespBean.error("用户名或密码错误!");
        }
        if (!userDetails.isEnabled()){
            return RespBean.error("用户状态异常!");
        }

        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userDetails,null, userDetails.getAuthorities());
        String jwt = jwtUtil.foundJWT(userDetails);
        SecurityContextHolder.getContext().setAuthentication(token);
        Map<String,String> msg = new HashMap<>();
        msg.put("tokenHead",tokenHead);
        msg.put("token", jwt);
        return RespBean.success("登录成功!",msg);
    }
}

7.1 完善 Swagger2Config 配置

由于 JWT 的加入,所以 Swagger2 的方法请求也是需要带入 JWT 令牌,提供了 Security 的全局认证。

只展示了修改的部分。

@Bean
public Docket docket(){
    return new Docket(DocumentationType.SWAGGER_2)
        .apiInfo(apiInfo()) // 配置 apiInfo
        .select() // 选择那些路径和api会生成document
        .apis(RequestHandlerSelectors.basePackage("com.cnda.controller")) // // 对哪些 api进行监控,RequestHandlerSelectors.basePackage 基于包扫描
        .paths(PathSelectors.any()) // 对所有路径进行监控
        .build()
        // 添加和 Security 相关的配置。
        .securityContexts(securityContexts())
        .securitySchemes(securitySchemes());
}

// 以下方法相对于给 Swagger 添加了一个在 Security 的全局授权,并且以正则的形式设置了授权的请求 url

/**
     * securityContexts
     * 请求体内容
     */
private List<SecurityContext> securityContexts(){
    List<SecurityContext> securityContexts = new ArrayList<>();
    securityContexts.add(getContextByPath("/hello/.*"));
    return securityContexts;
}

// 通过正则表达式来设置哪些路径
// 通过 Path 获取到对应的 SecurityContext
private SecurityContext getContextByPath(String pathRegex) {
    return SecurityContext.builder()
        .securityReferences(defaultAuth())
        .forPaths(PathSelectors.regex(pathRegex)) // 按照 String 的 matches 方法进行匹配
        .build();
}

/**
     * 配置默认的全局鉴权策略;其中返回的 SecurityReference 中,reference 即为 ApiKey 对象里面的name,保持一致才能开启全局鉴权
     * @return SecurityReference
     */
private List<SecurityReference> defaultAuth() {
    List<SecurityReference> references = new ArrayList<>();
    // scope 参数:
    AuthorizationScope authorizationScope = new AuthorizationScope("global","accessEverything");
    AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
    authorizationScopes[0] = authorizationScope;
    references.add(new SecurityReference("Authorization",authorizationScopes));
    return references;

}


/**
     * securitySchemes
     * 安全体方案
     */
private List<SecurityScheme> securitySchemes(){
    List<SecurityScheme> apiKeys = new ArrayList<>();
    // 设置请求头信息
    apiKeys.add(new ApiKey("Authorization","Authorization","Header"));
    return apiKeys;
}

修改的部分直接 CV 大法即可。

7.2 运行项目查看效果:

可以看到利用 Swagger2 的调试,返回 JWT Token 令牌成功!

{
  "code": 200,
  "message": "登录成功!",
  "obj": {
    "tokenHead": "Bearer",
    "token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImlidCI6MTY3Nzk4NzIwNjgyMSwiZXhwIjoxNjc4NTkyMDA2fQ.p_GUqevx8gvCK2txxeEX-RQFm69yDCxCYNlZbeHgVIizSUDO6gaT3a2jGXvzXqofH2uxkQBgN4WfeSIlGydiNA"
  }
}

将令牌设置到 Swagger2 中

这样之前的 /hello1 就可以请求成功了:

说明 Swagger2 设置 JWT 也成功了,每次发送请求,头部都会携带 JWT 令牌。

总结

还是对 Security 不太熟悉,Swagger2 的配置比较固定

JWT 主要也是两个点:

  • JWT Token Utile 工具类,主要用于管理 JWT 令牌。
  • JWT Token Filter JWT 拦截器,这个就是 Security 和 JWT 的集成了,以及请求发来的时候解析 JWT 从而完成免登录这一操作。

有关Spring Security + JWT + Swagger2 登录验证一套流程小结的更多相关文章

  1. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  2. ruby - 具有身份验证的私有(private) Ruby Gem 服务器 - 2

    我想安装一个带有一些身份验证的私有(private)Rubygem服务器。我希望能够使用公共(public)Ubuntu服务器托管内部gem。我读到了http://docs.rubygems.org/read/chapter/18.但是那个没有身份验证-如我所见。然后我读到了https://github.com/cwninja/geminabox.但是当我使用基本身份验证(他们在他们的Wiki中有)时,它会提示从我的服务器获取源。所以。如何制作带有身份验证的私有(private)Rubygem服务器?这是不可能的吗?谢谢。编辑:Geminabox问题。我尝试“捆绑”以安装新的gem..

  3. ruby-on-rails - 如果为空或不验证数值,则使属性默认为 0 - 2

    我希望我的UserPrice模型的属性在它们为空或不验证数值时默认为0。这些属性是tax_rate、shipping_cost和price。classCreateUserPrices8,:scale=>2t.decimal:tax_rate,:precision=>8,:scale=>2t.decimal:shipping_cost,:precision=>8,:scale=>2endendend起初,我将所有3列的:default=>0放在表格中,但我不想要这样,因为它已经填充了字段,我想使用占位符。这是我的UserPrice模型:classUserPrice回答before_val

  4. ruby-on-rails - 如何验证非模型(甚至非对象)字段 - 2

    我有一个表单,其中有很多字段取自数组(而不是模型或对象)。我如何验证这些字段的存在?solve_problem_pathdo|f|%>... 最佳答案 创建一个简单的类来包装请求参数并使用ActiveModel::Validations。#definedsomewhere,atthesimplest:require'ostruct'classSolvetrue#youcouldevencheckthesolutionwithavalidatorvalidatedoerrors.add(:base,"WRONG!!!")unlesss

  5. ruby-on-rails - 如何将验证与模型分开 - 2

    我有一些非常大的模型,我必须将它们迁移到最新版本的Rails。这些模型有相当多的验证(User有大约50个验证)。是否可以将所有这些验证移动到另一个文件中?说app/models/validations/user_validations.rb。如果可以,有人可以提供示例吗? 最佳答案 您可以为此使用关注点:#app/models/validations/user_validations.rbrequire'active_support/concern'moduleUserValidationsextendActiveSupport:

  6. ruby-on-rails - 跳过状态机方法的所有验证 - 2

    当我的预订模型通过rake任务在状态机上转换时,我试图找出如何跳过对ActiveRecord对象的特定实例的验证。我想在reservation.close时跳过所有验证!叫做。希望调用reservation.close!(:validate=>false)之类的东西。仅供引用,我们正在使用https://github.com/pluginaweek/state_machine用于状态机。这是我的预订模型的示例。classReservation["requested","negotiating","approved"])}state_machine:initial=>'requested

  7. ruby - 如何在 Rails 4 中使用表单对象之前的验证回调? - 2

    我有一个服务模型/表及其注册表。在表单中,我几乎拥有服务的所有字段,但我想在验证服务对象之前自动设置其中一些值。示例:--服务Controller#创建Action:defcreate@service=Service.new@service_form=ServiceFormObject.new(@service)@service_form.validate(params[:service_form_object])and@service_form.saverespond_with(@service_form,location:admin_services_path)end在验证@ser

  8. ruby - 如何验证 IO.copy_stream 是否成功 - 2

    这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下

  9. ruby-on-rails - ruby on rails 模型验证中的浮点精度 - 2

    我正在尝试使用正则表达式验证美元金额:^[0-9]+\.[0-9]{2}$这工作正常,但每当用户提交表单并且美元金额以0(零)结尾时,ruby(或rails?)将0砍掉。所以500.00变成500.0,因此正则表达式验证失败。有没有办法让ruby​​/rails保持用户输入的格式,而不管尾随零? 最佳答案 我假设您的美元金额是小数类型。因此,用户在字段中输入的任何值在保存到数据库之前都会从字符串转换为适当的类型。验证适用于已转换为数字类型的值,因此在您的情况下,正则表达式并不是真正合适的验证过滤器。不过,您有几种可能性可以解决这个问

  10. ruby-on-rails - 嵌套模型验证 - 错误不显示 - 2

    关于这个有很多问题,但似乎都没有帮助。是的,我看过thisrailscast.我有一个作者,他有很多书,像这样:作者:classAuthor书:classBook我创建了以下表单以在authors#show中向作者添加一本书:#labelsandbuttons......使用以下authors_controller方法:defshow@author=Author.find(params[:id])@book=@author.books.buildend...以及以下books_controller方法:defcreate@author=Author.find(params[:autho

随机推荐