Skip to content

四、快速开始(Quick Start)

ZeroOrInfinity edited this page Dec 31, 2020 · 20 revisions

1. 添加依赖(Add Dependency):

<dependency>
    <groupId>top.dcenter</groupId>
    <artifactId>ums-spring-boot-starter</artifactId>
    <version>[2.2.2,)</version>
</dependency>

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

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

<!-- 第三方授权登录默认会按照下面的优先级自行寻找一种 HTTP 工具依赖,java 11 HttpClient -> OkHttp3 -> apache HttpClient -> hutool-http
     示例使用 apache HttpClient .
     注意: 如果是 JDK11 则不需要此依赖-->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.12</version>
</dependency>

2. config:

server:
  port: 9090

spring:
  profiles:
    active: dev
  # mysql
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/ums?useSSL=false&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
    username: root
    password: 123456

  # session 简单配置
  session:
    # session 存储模式设置, 要导入相应的 spring-session 类的依赖, 默认为 none, 分布式应用把 session 放入 redis 等中间件
    store-type: none
    # session 过期时间
    timeout: PT300s

  # thymeleaf
  thymeleaf:
    encoding: utf-8
    prefix: classpath:/templates/
    suffix: .htm
    servlet:
      content-type: text/html;charset=UTF-8


# ums core
ums:
  # ================ 第三方授权登录相关配置 ================
  oauth:
    # 是否支持第三方授权登录功能, 默认: 空, 必须明确配置是否支持
    enabled: true
    # 第三方服务商: providerId
    github:
      # 根据是否有设置 clientId 来动态加载相应 JustAuth 的 AuthXxxRequest
      client-id: 4d4ee00e82f669f2ea8d
      client-secret: 953ddbe871a08d6924053531e89ecc01d87195a8
    gitee:
      client-id: dcc38c801ee88f43cfc1d5c52ec579751c12610c37b87428331bd6694056648e
      client-secret: f7384969d0e93088cdab546a1cea026442c16e2476cedec02e202cc89ee773ae
    # 第三方登录授权登录 url 前缀, 不包含 ServletContextPath,默认为 /auth2/authorization.
    auth-login-url-prefix: /auth2/authorization
    # 第三方登录回调处理 url 前缀 ,也就是 RedirectUrl 的前缀, 不包含 ServletContextPath,默认为 /auth2/login.
    redirect-url-prefix: /auth2/login
    # 第三方登录回调的域名, 例如:http://localhost 默认为 "http://127.0.0.1",
    # redirectUrl 直接由 {domain}/{servletContextPath}/{redirectUrlPrefix}/{providerId}(ums.oauth.[qq/gitee/weibo])组成
    domain: http://localhost:9090
    proxy:
      # 用于国内代理(HttpClient)超时, 默认 PT3S
      timeout: PT3S
      # 用于国外网站代理(HttpClient)超时, 默认 PT15S
      foreign-timeout: PT150S

  # RBAC 权限访问控制
  rbac:
    # ========= 放行权限 ==========
    # 权限表达式, 当 enableRestfulApi=true 且没有 @EnableGlobalMethodSecurity 注释时生效, 默认为 hasPermission(request, authentication).
    # hasPermission 表达式默认实现为 UriAuthoritiesPermissionEvaluator, 想自定义逻辑, 实现 PermissionEvaluator 即可替换.
    # String accessExp = "hasPermission(request, authentication)";
    # // 配置等效与
    # httpSecurity.authorizeRequests().anyRequest().access(hasPermission(request, authentication));
    restful-access-exp: permitAll()
    # 是否支持 restful Api (前后端交互接口的风格; 如: 查询(GET),添加(POST),修改(PUT),删除(DELETE)), 默认: true.
    # 当 {@code enableRestfulApi=false} 时 {@code accessExp} 权限表达式生效,
    # 当 {@code enableRestfulApi=true} 时 {@code restfulAccessExp} 权限表达式生效.
    enable-restful-api: true

  client:
    # ===================
    # 一级域名(不包括二级域名) 例如:
    #       domain: www.example.com -> topDomain: example.com
    #       domain: www.example.com.cn -> topDomain: example.com.cn
    #       domain: aaa.bbb.example.net -> topDomain: example.net
    # 测试时用的 IP 或 localhost 直接原样设置就行.
    # 在应用启动时通过 SecurityAutoConfiguration 自动注入 MvcUtil 字段 topDomain 中. 如在设置跨域 cookie 时可以通过 MvcUtil.getTopDomain() 方法获取.
    topDomain: localhost
    # ================ 密码登录, session, remember-me, csrf等配置 ================
    # 设置登录后返回格式(REDIRECT 与 JSON): 默认 JSON
    login-process-type: redirect
    # 登录页(必须自己实现)
    login-page: /login
    # 登录失败页(必须自己实现)
    failure-url: /login
    # 登录成功页(必须自己实现)
    success-url: /
    # 设置登出 url, 默认为 /logout
    logout-url: /logout
    # 设置登出后跳转的 url(必须自己实现), 默认为 /login
    logout-success-url: /login
    # 不需要认证的静态资源 urls, 例如: /resources/**, /static/**
    ignoring-urls:
      - /static/**
    # 不需要认证的 uri(可以带 HttpMethod 后缀; 用:隔开), 例如: /user/** 或 /user/**:post, 默认为 空 Set.
    permit-urls:
      - /hello:GET
      - /login
    # 设置登录时用户名的 request 参数名称, 默认为 username
    usernameParameter: username
    # 设置登录时用户密码的 request 参数名称, 默认为 password
    passwordParameter: password

  # ================ 验证码配置 ================
  # 同一个 uri 由多种验证码同时配置, **优先级**如下:
  #  `SMS > CUSTOMIZE > SELECTION > TRACK > SLIDER > IMAGE`
  codes:
    # 图片验证码
    image:
      # 设置需要图片验证码认证的 uri(必须是非 GET 请求),多个 uri 用 “-” 或 ","号分开支持通配符,如:/hello,/user/*;默认为 /authentication/form
      auth-urls:
        - /authentication/form
      request-param-image-code-name: imageCode
    # 短信验证码
    sms:
      # 设置需要短信验证码认证的 uri(必须是非 GET 请求),多个 uri 用 “,”号分开支持通配符,如:/hello,/user/*;默认为 /authentication/form
      auth-urls:
        - /authentication/mobile
      request-param-mobile-name: mobile
      request-param-sms-code-name: smsCode

  # ================ 手机登录配置 ================
  mobile:
    login:
      # 手机验证码登录是否开启, 默认 false,
      # 手机验证码登录开启后 必须配置 ums.codes.sms.auth-urls=/authentication/mobile
      sms-code-login-is-open: true
      # 手机验证码登录请求处理url, 默认 /authentication/mobile
      login-processing-url-mobile: /authentication/mobile

  # ================ 签到配置 ================
  sign:
    # redis key(String) 转 byte[] 转换时所用的 charset, 默认: StandardCharsets.UTF_8
    charset: UTF-8
    # 用于 redis 签到 key 前缀,默认为: u:sign:
    sign-key-prefix: 'u:sign:'
    # 用于 redis 总签到 key 前缀,默认为: total:sign:
    total-sign-key-prefix: 'total:sign:'
    # 获取最近几天的签到情况, 不能大于 28 天, 默认为 7 天
    last-few-days: 7
    # 用户签到 redis key TTL, 默认: 二个月 , 单位: 秒
    total-expired: 5356800
    # 用户签到统计 redis key TTL, 默认: 二个月 , 单位: 秒
    user-expired: 2678400

---
spring:
  profiles: dev
  mvc:
    throw-exception-if-no-handler-found: true
  thymeleaf:
    cache: false

#debug: true

server:
  port: 9090
  servlet:
    context-path: /demo

logging:
  config: classpath:logback-spring.xml

logback-spring.xml

<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml" />

    <!-- 控制台 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 日志格式 -->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID:- } --- [%thread] %X{MDC_TRACE_ID} %logger[%L] - %msg%n</pattern>
            <charset>utf-8</charset>
        </encoder>
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <!-- 只有这个日志权限才能看,sql语句 -->
            <level>DEBUG</level>
        </filter>
    </appender>
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
    </root>
    <logger name="top.dcenter" level="INFO" additivity="false">
        <appender-ref ref="STDOUT"/>
    </logger>
    <logger name="demo" level="INFO" additivity="false">
        <appender-ref ref="STDOUT"/>
    </logger>
</configuration>

3. 实现(implement) UmsUserDetailsService 接口等:

UserDetailsService.java

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import me.zhyd.oauth.model.AuthUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.lang.NonNull;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserCache;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletWebRequest;
import top.dcenter.ums.security.common.enums.ErrorCodeEnum;
import top.dcenter.ums.security.core.api.service.UmsUserDetailsService;
import top.dcenter.ums.security.core.exception.RegisterUserFailureException;
import top.dcenter.ums.security.core.exception.UserNotExistException;

import java.util.ArrayList;
import java.util.List;

/**
 *  用户密码与手机短信登录与注册服务:<br><br>
 *  1. 用于第三方登录与手机短信登录逻辑。<br><br>
 *  2. 用于用户密码登录逻辑。<br><br>
 *  3. 用户注册逻辑。<br><br>
 * @author YongWu zheng
 * @version V1.0  Created by 2020/9/20 11:06
 */
@Service
public class UserDetailsServiceImpl implements UmsUserDetailsService {

    private final Logger log = LoggerFactory.getLogger(this.getClass());
    /**
     * 用户名
     */
    public static final String PARAM_USERNAME = "username";

    /**
     * 密码
     */
    public static final String PARAM_PASSWORD = "password";

    private final ObjectMapper objectMapper;

    private final JdbcTemplate jdbcTemplate;

    @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
    @Autowired(required = false)
    private UserCache userCache;
    /**
     * 用于密码加解密
     */
    @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
    @Autowired
    private PasswordEncoder passwordEncoder;

    public UserDetailsServiceImpl(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
        this.objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    @SuppressWarnings("AlibabaUndefineMagicConstant")
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        try
        {
            // 从缓存中查询用户信息:
            // 从缓存中查询用户信息
            if (this.userCache != null)
            {
                UserDetails userDetails = this.userCache.getUserFromCache(username);
                if (userDetails != null)
                {
                    return userDetails;
                }
            }
            // 根据用户名获取用户信息

            // 获取用户信息逻辑。。。
            // ...

            // 示例:只是从用户登录日志表中提取的信息,
            log.info("Demo ======>: 登录用户名:{}, 登录成功", username);
            return new User(username,
                            passwordEncoder.encode("admin"),
                            true,
                            true,
                            true,
                            true,
                            AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_VISIT, ROLE_USER"));

        }
        catch (Exception e)
        {
            String msg = String.format("Demo ======>: 登录用户名:%s, 登录失败: %s", username, e.getMessage());
            log.error(msg, e);
            throw new UserNotExistException(ErrorCodeEnum.QUERY_USER_INFO_ERROR, e, username);
        }
    }


    @Override
    public UserDetails registerUser(String mobile) throws RegisterUserFailureException {

        if (mobile == null)
        {
            throw new RegisterUserFailureException(ErrorCodeEnum.MOBILE_NOT_EMPTY, null);
        }

        // 用户信息持久化逻辑。。。
        // ...

        log.info("Demo ======>: 手机短信登录用户 {}:注册成功", mobile);

        User user = new User(mobile,
                             passwordEncoder.encode("admin"),
                             true,
                             true,
                             true,
                             true,
                             AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_VISIT, ROLE_USER")
        );

        // 把用户信息存入缓存
        if (userCache != null)
        {
            userCache.putUserInCache(user);
        }

        return user;
    }

    @Override
    public UserDetails registerUser(ServletWebRequest request) throws RegisterUserFailureException {

        String username = getValueOfRequest(request, PARAM_USERNAME, ErrorCodeEnum.USERNAME_NOT_EMPTY);
        String password = getValueOfRequest(request, PARAM_PASSWORD, ErrorCodeEnum.PASSWORD_NOT_EMPTY);
        // ...

        // UserInfo userInfo = getUserInfo(request)

        // 用户信息持久化逻辑。。。
        // ...

        String encodedPassword = passwordEncoder.encode(password);

        log.info("Demo ======>: 用户名:{}, 注册成功", username);
        User user = new User(username,
                             encodedPassword,
                             true,
                             true,
                             true,
                             true,
                             AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_VISIT, ROLE_USER")
        );

        // 把用户信息存入缓存
        if (userCache != null)
        {
            userCache.putUserInCache(user);
        }

        return user;

    }

    @Override
    public UserDetails registerUser(@NonNull AuthUser authUser, @NonNull String username, @NonNull String defaultAuthority,
                                    String decodeState) throws RegisterUserFailureException {

        // 第三方授权登录不需要密码, 这里随便设置的, 生成环境按自己的逻辑
        String encodedPassword = passwordEncoder.encode(authUser.getUuid());

        // 这里的 decodeState 可以根据自己实现的 top.dcenter.ums.security.core.oauth.service.Auth2StateCoder 接口的逻辑来传递必要的参数.
        // 比如: 第三方登录成功后的跳转地址
        final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 假设 decodeState 就是 redirectUrl, 我们直接把 redirectUrl 设置到 request 上
        // 后续经过成功处理器时直接从 requestAttributes.getAttribute("redirectUrl", RequestAttributes.SCOPE_REQUEST) 获取并跳转
        if (requestAttributes != null) {
            requestAttributes.setAttribute("redirectUrl", decodeState, RequestAttributes.SCOPE_REQUEST);
        }
        // 当然 decodeState 也可以传递从前端传到后端的用户信息, 注册到本地用户

        List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList(defaultAuthority);

        // ... 用户注册逻辑

        log.info("Demo ======>: 用户名:{}, 注册成功", username);

        // @formatter:off
        UserDetails user = User.builder()
                               .username(username)
                               .password(encodedPassword)
                               .disabled(false)
                               .accountExpired(false)
                               .accountLocked(false)
                               .credentialsExpired(false)
                               .authorities(grantedAuthorities)
                               .build();
        // @formatter:off

        // 把用户信息存入缓存
        if (userCache != null)
        {
            userCache.putUserInCache(user);
        }

        return user;
    }

    @Override
    public UserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
        UserDetails userDetails = loadUserByUsername(userId);
        User.withUserDetails(userDetails);
        return User.withUserDetails(userDetails).build();
    }

    @Override
    public List<Boolean> existedByUsernames(String... usernames) throws UsernameNotFoundException {
        // ... 在本地账户上查询 userIds 是否已被使用
        List<Boolean> list = new ArrayList<>();
        list.add(true);
        list.add(false);
        list.add(false);

        return list;
    }

    private String getValueOfRequest(ServletWebRequest request, String paramName, ErrorCodeEnum usernameNotEmpty) throws RegisterUserFailureException {
        String result = request.getParameter(paramName);
        if (result == null)
        {
            throw new RegisterUserFailureException(usernameNotEmpty, request.getSessionId());
        }
        return result;
    }
}

DemoUriAuthorizeServiceImpl.java

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import top.dcenter.ums.security.common.enums.ErrorCodeEnum;
import top.dcenter.ums.security.core.api.permission.service.AbstractUriAuthorizeService;
import top.dcenter.ums.security.core.vo.ResponseResult;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import static top.dcenter.ums.security.core.util.AuthenticationUtil.responseWithJson;

/**
 * @author YongWu zheng
 * @version V2.0  Created by 2020/11/11 17:09
 */
@Slf4j
public class DemoUriAuthorizeServiceImpl extends AbstractUriAuthorizeService {

    @SuppressWarnings("SpringJavaAutowiredMembersInspection")
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void handlerError(int status, HttpServletResponse response) {

        try
        {
            responseWithJson(response, status, objectMapper.writeValueAsString(ResponseResult.fail(ErrorCodeEnum.PERMISSION_DENY)));
        }
        catch (IOException e)
        {
            log.error(String.format("权限控制错误响应异常: status=%s, error=%s", status, e.getMessage()), e);
        }
    }

    @Override
    public Map<String, Map<String, Set<String>>> getRolesAuthorities() {
        // do nothing 具体看 permission-example 的 demo.permission.service.impl.UriAuthorizeServiceImpl
        return new HashMap<>(0);
    }

}

UserController.java

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

/**
 *
 * @author YongWu zheng
 * @version V1.0  Created by 2020/9/20 20:04
 */
@Controller
@Slf4j
public class UserController {

    @GetMapping("/login")
    public String login() {
        return "login";
    }

    @GetMapping("/")
    public String index(@AuthenticationPrincipal UserDetails userDetails, Model model) {
        if (userDetails != null)
        {
            model.addAttribute("username", userDetails.getUsername());
            model.addAttribute("roles", userDetails.getAuthorities());
        }
        else
        {
            model.addAttribute("username", "anonymous");
            model.addAttribute("roles", "ROLE_VISIT");
        }
        return "index";
    }

    @GetMapping("/me")
    @ResponseBody
    public Object getCurrentUser(@AuthenticationPrincipal UserDetails userDetails, Authentication authentication) {
        
        Map<String, Object> map = new HashMap<>(16);
        map.put("authenticationHolder", SecurityContextHolder.getContext().getAuthentication());
        map.put("userDetails", userDetails);
        map.put("authentication", authentication);
        return map;
    }

}

4. 前端页面 :

login.htm: 放在 classpath:/templates/

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jquery@1.11.1/dist/jquery.min.js"></script>
    <style>
        #sent_sms_btn{
            background-color: #efefef
        }
    </style>
</head>
<body>
<h2>登录页面</h2>
<h3>表单登录</h3>
<h5>如果短信验证码与图片验证码同时配置时,优先使用短信验证码,图片验证码失效</h5>
<!-- 通过 th:action 的方式支持 csrf 或者 添加隐藏域<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> -->
<form id="reg-form" th:action="@{/authentication/form}" method="post">
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" name="username" value="admin" ><p style="color: #ff0000"
                                                                     id="error-name"></p></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="password" name="password" value="admin"></td>
        </tr>
        <tr>
            <td>图形验证码:</td>
            <td>
                <input type="text" name="imageCode">
                <img class="img" th:src="@{/code/image}" style="width: 67px; height: 23px">
            </td>
        </tr>
        <tr>
            <td ><input type="checkbox" name="rememberMe" checked="true">记住我</input></td>
            <td><p style="color: #ff0000" id="error-code"></p></td>
        </tr>
        <tr>
            <td ><button id="btn-reg" type="button">登录ajax</button></td>
            <!-- 通过 form submit 不能接收错误信息, 通过 ajax 可接收错误信息 -->
            <td ><button type="submit">登录</button></td>
        </tr>
    </table>
</form>
<h3>手机登录</h3>
<form id="mobile-form" th:action="@{/authentication/mobile}" method="post">
    <table>
        <tr>
            <td>手机号码:</td>
            <td>
                <input type="tel" name="mobile" value="13345678980"><button type="button" id="sent_sms_btn">发送验证码</button>
                <p style="color: #ff0000" id="error-name-mobile"></p>
                <a th:href="@{/code/sms}" id="sms_uri" hidden="hidden" />
            </td>
        </tr>
        <tr>
            <td>手机验证码:</td>
            <td>
                <input type="text" name="smsCode">
            </td>
        </tr>
        <tr>
            <td ><input type="checkbox" name="rememberMe" checked="true">记住我</input></td>
            <td><p style="color: #ff0000" id="error-code-mobile"></p></td>
        </tr>
        <tr>
            <td ><button id="btn-mobile" type="button">登录ajax</button></td>
        </tr>
    </table>
</form>
<br><br>
<h3>社交登录</h3>
<a th:href="@{/auth2/authorization/gitee}">gitee登录</a>
<a th:href="@{/auth2/authorization/github}">github登录</a>
<a th:href="@{/auth2/authorization/gitee}">github登录</a>

<dev id="basePath" th:basePath="@{/}" style="display: none"/>
</body>
<script>
    var basePath = $("#basePath").attr("basePath");
    $.fn.serializeObject = function()
    {
        let o = {};
        let a = this.serializeArray();
        $.each(a, function() {
            if (o[this.name]) {
                if (!o[this.name].push) {
                    o[this.name] = [o[this.name]];
                }
                o[this.name].push(this.value || '');
            } else {
                o[this.name] = this.value || '';
            }
        });
        return o;
    }

    $(".img").click(function(){
        let uri = this.getAttribute("src");
        console.log(uri)
        let end = uri.indexOf('?', 0);
        console.log(end)
        if (end === -1) {
            uri = uri + '?'+ Math.random();
        } else {
            uri = uri.substring(0, end) + '?'+ Math.random();
        }
        console.log(uri)
        this.setAttribute('src', uri);
    });

    $("#sent_sms_btn").click(function () {
        let uri = $("#sms_uri").attr("href");
        console.log(document.getElementById("sent_sms_btn").parentElement.getElementsByTagName("input")[0]);
        let mobile =
            document.getElementById("sent_sms_btn").parentElement.getElementsByTagName("input")[0].value;
        console.log(mobile);
        uri = uri + "?mobile=" + mobile;
        console.log(uri);
        $.ajax({
            url: uri,
            type: "GET",
            dataType: "json",
            contentType: 'application/json; charset=UTF-8',
            success: function (data) {
                console.log("==========发生短信验证码成功============")
                // ...
                console.log(data)
                $("#error-name-mobile").text("")
                $("#error-code-mobile").text("")
                let expireIn = data.data
                $("#sent_sms_btn:button:not(:disabled)").attr("di", "di").attr("disabled", true).css("background-color", "#c2c2c2");
                let interval = setInterval(function () {
                    $("#sent_sms_btn").text("剩余 " + (expireIn--) + " 秒")
                    if (expireIn < 0) {
                        $("#sent_sms_btn").text("发送验证码")
                        $("#sent_sms_btn:button[di=di]").attr("disabled", false).removeAttr("di").css("background-color", "#efefef");
                        window.clearInterval(interval);
                    }
                }, 1000);

            },
            error: function (data) {
                console.log("==========发送短信验证码失败============")
                console.log(data)
                $("#error-name-mobile").text("")
                $("#error-code-mobile").text("")
                data = data.responseJSON
                if (undefined !== data) {
                    console.log(data);
                    // 错误代码看ErrorCodeEnum
                    if (data.code >= 900 && data.code < 1000) {
                        $("#error-name-mobile").text(data.msg)
                    } else if (data.code >= 600 && data.code < 700) {
                        $("#error-code-mobile").text(data.msg)
                    }
                }
            }
        })

    });

    function submitFormByAjax(url, formId, errorNameId, errorCodeId, imgId, refresh) {
        return function () {
            console.log(JSON.stringify($(formId).serializeObject()))
            $.ajax({
                // 如果用的是模板,则 url 可以使用注入的方式,会跟着配置动态改变
                url: url,
                data: JSON.stringify($(formId).serializeObject()),
                type: "POST",
                dataType: "json",
                contentType: 'application/json; charset=UTF-8',
                success: function (data) {
                    $(errorNameId).text("")
                    $(errorCodeId).text("")
                    console.log("==========注册成功============")
                    // 注册成功
                    // ...
                    console.log(data)
                    let uri = data.data.targetUrl
                    if (uri === null) {
                        uri = basePath
                    }
                    window.location.href = uri;
                },
                error: function (data) {
                    // 注册失败
                    $(errorNameId).text("")
                    $(errorCodeId).text("")
                    console.log("********注册失败*********")
                    console.log(data)
                    data = data.responseJSON
                    if (undefined !== data) {
                        console.log(data);
                        // 错误代码看ErrorCodeEnum
                        if (data.code >= 900 && data.code < 1000) {
                            $(errorNameId).text(data.msg)
                        } else if (data.code >= 600 && data.code < 700) {
                            $(errorCodeId).text(data.msg)
                        }
                    }
                    if (refresh) {
                        $(imgId).trigger("click");
                    }
                }
            })
            return
        };
    }


    $("#btn-mobile").click(
        submitFormByAjax($("#mobile-form").attr("action"), "#mobile-form", "#error-name-mobile", "#error-code-mobile", ".img-mobile", true)
    )


    $("#btn-reg").click(
        submitFormByAjax($("#reg-form").attr("action"), "#reg-form", "#error-name", "#error-code", ".img", true)
    )

</script>
</html>

index.htm

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
hello <span th:text
Clone this wiki locally