<repo_name>jeesun/thymelte<file_sep>/README.md
# thymelte

## 项目简介
这是一个基于Spring Boot+AdminLTE+Thymeleaf的管理端项目，目的是简便管理端的开发。
## 功能列表
- 登录页面：
1. 4种登录方式，用户名+密码，手机号+密码，邮箱+密码，app扫码；
2. 验证码；
- 管理页面：使用Thymeleaf将AdminLTE页面拆分成多个组件，降低耦合；
- 日志保存到文件并按日归档；
- Druid监控功能。访问[http://localhost:8090/druid/index.html](http://localhost:8090/druid/index.html)，用户名druid，密码<PASSWORD>。

## 截图
![账号密码登录](screenshots/2018-04-22_030148.png)

![扫码登录](screenshots/2018-04-22_030158.png)

![主页面](screenshots/2018-04-22_030226.png)

## 使用流程
1. 建表  
请参照`oauthserver`模块的说明文档，执行建表语句。  
`oauthserver`项目地址：
- [gitee](https://gitee.com/jeesun/oauthserver)
- [github](https://github.com/jeesun/oauthserver)<file_sep>/src/main/java/com/jeesun/thymelte/custom/CustomTokenAuthProvider.java
package com.jeesun.thymelte.custom;

import com.jeesun.thymelte.domain.QrCode;
import com.jeesun.thymelte.domain.UserEntity;
import com.jeesun.thymelte.repository.QrCodeRepository;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.Collection;

@Component
public class CustomTokenAuthProvider implements AuthenticationProvider {
    private static final Logger logger = Logger.getLogger(CustomTokenAuthProvider.class);
    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Autowired
    private QrCodeRepository qrCodeRepository;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        logger.error("authenticate");
        String username = authentication.getName();
        String password = (String) authentication.getCredentials();
        UserEntity userEntity = (UserEntity) userDetailsService.loadUserByUsername(username);
        if (null == userEntity){
            throw new BadCredentialsException("用户名错误");
        }

        String sid = "";
        QrCode qrCode = qrCodeRepository.findBySid(password);
        if(password.length() >= 32){
            //说明是uuid，此时是扫码登录
            sid = qrCode.getSid();
            if (username.equals(qrCode.getUsername())){
                logger.info("用户名和绑定的一样");
            }else{
                throw new BadCredentialsException("uuid错误");
            }
        }

        Collection<? extends GrantedAuthority> authorities = userEntity.getAuthorities();
        return new UsernameTokenAuthenticationToken(authorities, username, qrCode.getToken(), sid);
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return UsernameTokenAuthenticationToken.class.equals(aClass);
    }
}
<file_sep>/src/main/resources/static/js/login-qrcode.js
//扫码登录

var qrCode = new QRCode(document.getElementById("qrcode"), {
    width : 200,
    height : 200
});

var loopVar;

    function initQrCode(){
    console.log("initQrCode");

    setTokenInHeader();

    var qrCodeData = $.cookie('qrCodeData');
    //console.log(qrCodeData);
    if(!qrCodeData || "null" == qrCodeData){
        refreshQrCode();
    }else{
        qrCode.makeCode(qrCodeData);
        loop();
    }
}

function refreshQrCode() {
    $.getJSON("users/uuid", function (data) {
        //存cookie
        var expiresDate= new Date();
        expiresDate.setTime(expiresDate.getTime() + (2 * 60 * 1000));//2分钟过期
        $.cookie('qrCodeData', JSON.stringify(data),{
            expires: expiresDate
        });

        qrCode.makeCode(JSON.stringify(data));
        loop();
    });
}

//循环检查是否被扫码
function loop() {
    clearInterval(loopVar);//先清除上次的定时任务（防止用户反复在账号登录和二维码登录的图片间切换造成的多次循环检查问题。）
    loopVar = setInterval(function () {
        //获取qrCodeData
        var qrCodeData = $.cookie('qrCodeData');
        //检查qrCodeData是否失效
        if(!qrCodeData){
            console.log("二维码已失效");
            clearInterval(loop);
            $("#refreshQrCode").css("visibility","visible");
        }else{
            var cookieQrCode = JSON.parse(qrCodeData);
            var sid = cookieQrCode.sid;

            $.getJSON("users/loopCheck/" + sid, function(data){
                //console.log(data);
                if(data['code'] == 200){
                    clearInterval(loop);
                    $.cookie('qrCodeData', null);//赋值"null"字符串
                    $("#refreshQrCode").css("display", "none");
                    $("#jumpHint").css("display", "block");
                    setTimeout(function(){
                        window.location.href = "/index";
                    },2000);
                }else if(data['code'] == 500){
                    clearInterval(loopVar);
                    //console.log(data['message']);
                }
            });
        }
    }, 1000);
}

$("#refreshQrCode > a").click(function () {
    $("#refreshQrCode").css("visibility","hidden");
    initQrCode();
});<file_sep>/src/main/java/com/jeesun/thymelte/config/AuthSuccessHandler.java
package com.jeesun.thymelte.config;

import com.alibaba.fastjson.JSON;
import com.jeesun.thymelte.domain.LogLogin;
import com.jeesun.thymelte.domain.UserEntity;
import com.jeesun.thymelte.repository.LogLoginRepository;
import com.jeesun.thymelte.util.DateUtil;
import com.jeesun.thymelte.util.IpUtil;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Date;

@Component
public class AuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    private static Logger logger = Logger.getLogger(AuthSuccessHandler.class);

    @Autowired
    private LogLoginRepository logLoginRepository;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        UserEntity userDetails = (UserEntity) authentication.getPrincipal();
        LogLogin logLogin = new LogLogin();
        logLogin.setUsername(userDetails.getUsername());
        logLogin.setCreateTime(new Date());
        logLogin.setIp(IpUtil.getIpAddr(request));
        logLogin = logLoginRepository.save(logLogin);
        logger.info(JSON.toJSONString(logLogin));

        HttpSession httpSession = request.getSession();
        logger.info("session create time = " + DateUtil.format(httpSession.getCreationTime(), "yyyy-MM-dd HH:mm:ss"));

        super.onAuthenticationSuccess(request, response, authentication);
    }
}
<file_sep>/src/main/resources/static/js/file_upload.js
/**
 *
 * User: simon
 * Date: 2018/06/06
 * Time: 14:45
 **/
$(function () {
    'use strict';

    //必须加这段代码，不然无法上传
    var token = $("meta[name='_csrf']").attr("content");
    var header = $("meta[name='_csrf_header']").attr("content");
    $(document).ajaxSend(function (e, xhr, options) {
        xhr.setRequestHeader(header, token);
    });

    var uploader = $('#file_upload');
    uploader.fileupload({
        url: "uploadFiles/upload",
        dataType: 'json',
        type: "post",
        multipart: true,
        acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i,
        maxFileSize: 1000 * 1024 * 1024,
        maxNumberOfFiles: 50,
        change: function (e, data){
            $('.preview').empty();
            $('.progress-bar').css('width', '0%');
            $('.proportion').html('');
            $('input[name="imageUrl"]').remove();
        },
        done: function (e, data) {
            var result = data.result;
            //done方法就是上传完毕的回调函数，其他回调函数可以自行查看api
            //注意data要和jquery的ajax的data参数区分，这个对象包含了整个请求信息
            //返回的数据在data.result中，这里dataType中设置的返回的数据类型为json
            if(200 == result.code) {
                // 上传成功：
                $(".preview").append('<div style="margin-top:10px;"><img src="uploadFiles/file/'+result.data[0] + '"></div>');
                $(".preview").append("<div><button class='delete_file' type='button'>删除</button></div>");

                //设置filePath
                $('#file_paths').append('<input type="hidden" name="imageUrl" value="uploadFiles/file/' + result.data[0] + '">');
            } else {
                // 上传失败：
                $(".upstatus").append("<div style='color:red;'>"+result.msg+"</div>");
            }
        },messages: {
            maxFileSize: '文件大小超过限制',
            acceptFileTypes: '文件格式不支持'
        },progressall: function (e, data) {
            var progress = parseInt(data.loaded / data.total * 100, 10);
            $(".progress-bar").css("width", progress + "%");
            $(".proportion").html("上传总进度："+progress+"%");
        },processfail: function (e, data) {
            var currentFile = data.files[data.index];
            if (data.files.error && currentFile.error) {
                alert(currentFile.error);
            }
        }
    });

    $(document).on('click', '.delete_file', function () {
        console.log("clicked");
        $('.preview').empty();
        $('.progress-bar').css('width', '0%');
        $('.proportion').html('');
        $('input[name="imageUrl"]').remove();
    });
});<file_sep>/src/main/resources/static/js/table_hint.js
/**
 *
 * User: simon
 * Date: 2018/06/06
 * Time: 19:48
 **/
window.hideHint = function () {
    $('#hint').css('visibility', 'hidden');
};

window.showHint = function (message) {
    $('#hint').text(message);
    $('#hint').css('visibility', 'visible');
};<file_sep>/src/main/resources/static/plug-in/buttonLoader/jquery/plugin/jquery.buttonLoader.js
/*A jQuery plugin which add loading indicators into buttons
* By <NAME>
* MIT Licensed.
*/
(function ($) {
    $('.has-spinner').attr("disabled", false);
    $.fn.buttonLoader = function (action, message) {
        var self = $(this);
        if (action == 'loading') {
            if ($(self).attr("disabled") == "disabled") {
                return false;
            }
            $('.has-spinner').attr("disabled", true);
            $(self).attr('data-btn-text', $(self).text());
            var text = message;
            console.log($(self).attr('data-load-text'));
            if($(self).attr('data-load-text') != undefined && $(self).attr('data-load-text') != ""){
                text = $(self).attr('data-load-text');
            }
            $(self).html('<span class="spinner"><i class="fa fa-spinner fa-spin" title="button-loader"></i></span> '+text);
            $(self).addClass('active');
        }

        if (action == 'success') {
            $(self).html('<i class="fa fa-check" aria-hidden="true"></i> ' + message);
            $(self).removeClass('active');
            $('.has-spinner').attr("disabled", false);
            $('.has-spinner').removeClass('btn-default btn-primary');
            $('.has-spinner').addClass('btn-success');
        }

        if (action == 'error') {
            $(self).html('<i class="fa fa-close" aria-hidden="true"></i> ' + message);
            $(self).removeClass('active');
            $('.has-spinner').attr("disabled", false);
            $('.has-spinner').removeClass('btn-default btn-primary');
            $('.has-spinner').addClass('btn-danger');
        }
    }
})(jQuery);
<file_sep>/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.jeesun</groupId>
	<artifactId>thymelte</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>war</packaging>

	<name>thymelte</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.14.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-oauth2</artifactId>
		</dependency>
		<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.thymeleaf.extras</groupId>
			<artifactId>thymeleaf-extras-springsecurity4</artifactId>
			<version>2.1.2.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<!--<exclusions>
				<exclusion>
					<groupId>org.springframework.boot</groupId>
					<artifactId>spring-boot-starter-tomcat</artifactId>
				</exclusion>
			</exclusions>-->
		</dependency>
		<!-- 打war包时加入此项， 告诉spring-boot tomcat相关jar包用外部的，不要打进去 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
			<scope>provided</scope>
		</dependency>
		<!--<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jetty</artifactId>
		</dependency>-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-actuator</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-logging</artifactId>
		</dependency>

		<!-- rabbit mq -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>

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

		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>1.3.2</version>
		</dependency>
		<!-- 引入spring boot自带的pagehelper插件 -->
		<dependency>
			<groupId>com.github.pagehelper</groupId>
			<artifactId>pagehelper-spring-boot-starter</artifactId>
			<version>1.2.5</version>
		</dependency>

		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatisplus-spring-boot-starter</artifactId>
			<version>1.0.5</version>
		</dependency>


		<!-- 上传图片需要的依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-hateoas</artifactId>
		</dependency>
		<dependency>
			<groupId>com.jayway.jsonpath</groupId>
			<artifactId>json-path</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>com.github.ulisesbocchio</groupId>
			<artifactId>jasypt-spring-boot-starter</artifactId>
			<version>1.8</version>
		</dependency>

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

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

		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger-ui</artifactId>
			<version>2.6.1</version>
		</dependency>
        <!-- swagger2依赖end -->

		<!--<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
		</dependency>-->

		<!--启用不严格检查html-->
		<dependency>
			<groupId>net.sourceforge.nekohtml</groupId>
			<artifactId>nekohtml</artifactId>
			<version>1.9.22</version>
		</dependency>

		<dependency>
			<groupId>commons-io</groupId>
			<artifactId>commons-io</artifactId>
			<version>2.6</version>
		</dependency>
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
			<version>3.7</version>
		</dependency>
		<dependency>
			<groupId>commons-codec</groupId>
			<artifactId>commons-codec</artifactId>
			<version>1.11</version>
		</dependency>


		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>adminlte</artifactId>
			<version>2.4.2</version>
		</dependency>

		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>jquery</artifactId>
			<version>3.3.1</version>
		</dependency>

		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>bootstrap</artifactId>
			<version>3.3.7</version>
		</dependency>

		<dependency>
			<groupId>org.webjars.bower</groupId>
			<artifactId>font-awesome</artifactId>
			<version>4.7.0</version>
		</dependency>

		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>ionicons</artifactId>
			<version>2.0.1</version>
		</dependency>

		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>html5shiv</artifactId>
			<version>3.7.3</version>
		</dependency>

		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>respond</artifactId>
			<version>1.4.2</version>
		</dependency>

		<dependency>
			<groupId>org.webjars.bower</groupId>
			<artifactId>bootstrap-table</artifactId>
			<version>1.11.1</version>
		</dependency>

		<dependency>
			<groupId>org.webjars.bower</groupId>
			<artifactId>moment</artifactId>
			<version>2.21.0</version>
		</dependency>

		<dependency>
			<groupId>org.webjars.bower</groupId>
			<artifactId>jquery-validation</artifactId>
			<version>1.17.0</version>
		</dependency>

		<dependency>
			<groupId>org.webjars.bower</groupId>
			<artifactId>jquery-file-upload</artifactId>
			<version>9.18.0</version>
		</dependency>

		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>qrcodejs</artifactId>
			<version>1c78ccd</version>
		</dependency>

		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>jquery-cookie</artifactId>
			<version>1.4.1-1</version>
		</dependency>

		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>excanvas</artifactId>
			<version>3</version>
		</dependency>

        <dependency>
            <groupId>org.webjars.bower</groupId>
            <artifactId>echarts</artifactId>
            <version>4.0.4</version>
        </dependency>

		<dependency>
			<groupId>org.webjars.bower</groupId>
			<artifactId>toastr</artifactId>
			<version>2.1.3</version>
		</dependency>

		<!-- 自定义的jar start -->
		<dependency>
			<groupId>org.webjars.bower</groupId>
			<artifactId>FroalaWysiwygEditor</artifactId>
			<version>2.8.1</version>
		</dependency>
		<!-- 自定义的jar end -->

        <dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<version>9.4-1206-jdbc42</version>
		</dependency>

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

		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.1.9</version>
		</dependency>

		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.47</version>
		</dependency>

		<dependency>
			<groupId>com.google.code.gson</groupId>
			<artifactId>gson</artifactId>
			<version>2.8.5</version>
		</dependency>

		<dependency>
			<groupId>commons-fileupload</groupId>
			<artifactId>commons-fileupload</artifactId>
			<version>1.3.3</version>
		</dependency>

		<!-- Spring Boot打包成war包，需要的jar -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>javax.servlet-api</artifactId>
			<version>3.1.0</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>

		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-core</artifactId>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.datatype</groupId>
			<artifactId>jackson-datatype-joda</artifactId>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.module</groupId>
			<artifactId>jackson-module-parameter-names</artifactId>
		</dependency>

		<!-- MyBatis start -->
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>1.3.2</version>
		</dependency>
		<dependency>
			<groupId>org.mybatis.generator</groupId>
			<artifactId>mybatis-generator-maven-plugin</artifactId>
			<version>1.3.7</version>
		</dependency>
		<dependency>
			<groupId>org.mybatis.generator</groupId>
			<artifactId>mybatis-generator-core</artifactId>
			<version>1.3.7</version>
		</dependency>

		<!-- 分页插件 -->
		<dependency>
			<groupId>com.github.pagehelper</groupId>
			<artifactId>pagehelper-spring-boot-starter</artifactId>
			<version>1.2.5</version>
		</dependency>
		<!-- mybatis-plus插件 -->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>2.3</version>
		</dependency>
		<!-- 通用Mapper插件 -->
		<dependency>
			<groupId>tk.mybatis</groupId>
			<artifactId>mapper-spring-boot-starter</artifactId>
			<version>2.0.3</version>
		</dependency>
		<dependency>
			<groupId>tk.mybatis</groupId>
			<artifactId>mapper-generator</artifactId>
			<version>1.0.4</version>
		</dependency>

		<dependency>
			<groupId>org.freemarker</groupId>
			<artifactId>freemarker</artifactId>
			<version>2.3.27-incubating</version>
		</dependency>
		<dependency>
			<groupId>cn.hutool</groupId>
			<artifactId>hutool-core</artifactId>
			<version>4.1.5</version>
		</dependency>

		<!-- MyBatis end -->
	</dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>Camden.SR5</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>

			<!-- Spring Boot項目打包成war，默认会带版本号。此处设置不带版本号。 -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-war-plugin</artifactId>
				<configuration>
					<warName>thymelte</warName>
				</configuration>
			</plugin>
			<!-- mybatis generator 自动生成代码插件 -->
			<plugin>
				<groupId>org.mybatis.generator</groupId>
				<artifactId>mybatis-generator-maven-plugin</artifactId>
				<version>1.3.6</version>
				<configuration>
					<configurationFile>${basedir}/src/main/resources/generator/generatorConfig.xml</configurationFile>
					<overwrite>true</overwrite>
					<verbose>true</verbose>
				</configuration>
				<executions>
					<execution>
						<id>Generate MyBatis Artifacts</id>
						<goals>
							<goal>generate</goal>
						</goals>
					</execution>
				</executions>
				<dependencies>
					<!-- 配置这个依赖主要是为了等下在配置MG的时候可以不用配置classPathEntry这样的一个属性 -->
					<!-- 避免代码的耦合度太高 -->
					<dependency>
						<groupId>org.postgresql</groupId>
						<artifactId>postgresql</artifactId>
						<version>9.4-1206-jdbc42</version>
					</dependency>
				</dependencies>
			</plugin>
			<plugin>
				<groupId>pl.project13.maven</groupId>
				<artifactId>git-commit-id-plugin</artifactId>
				<configuration>
					<dotGitDirectory>${project.basedir}/.git</dotGitDirectory>
				</configuration>
			</plugin>
		</plugins>
		<resources>
			<!-- 如果mapper.xml是放在src/main/java目录下，需配置以下-->
			<!--<resource>
				<directory>src/main/java</directory>
				<filtering>false</filtering>
				<includes>
					<include>**/mapping/*.xml</include>
				</includes>
			</resource>-->

			<!-- 解决yml配置文件无法使用@..@占位符引用Maven项目的属性 -->
			<resource>
				<directory>src/main/resources</directory>
				<filtering>true</filtering>
			</resource>
		</resources>
	</build>

</project>
<file_sep>/src/main/resources/messages.properties
usernameNotFound=\u7528\u6237\u540d\u4e0d\u5b58\u5728
accountDisabled=\u60a8\u5df2\u88ab\u5c01\u53f7
passwordError=\<PASSWORD>
tokenWasNotRecognised=\<KEY>
helloWorld=\u4f60\u597d\uff0c\u4e16\u754c
invalidToken=\u<PASSWORD>\u6548\u7684\u<PASSWORD>\u<PASSWORD>\u<PASSWORD>\<PASSWORD>\<PASSWORD>
approve=\u540c\u610f
deny=\u62d2\u7edd
authorize=\u6388\u6743
appApproval=\u5e94\u7528\u6388\u6743
doYouAuthorize=\u4f60\u540c\u610f\u6388\u6743\u5e94\u7528
toAccessYouProtectedResources=\u83b7\u53d6\u60a8\u7684\u53d7\u4fdd\u62a4\u7684\u8d44\u6e90\u5417\uff1f
login=\u767b\u5f55
signIn=\u767b\u5f55
username=\u7528\u6237\u540d
password=\<PASSWORD>
error=\u9519\u8bef
userNotValid=\u7528\u6237\u65e0\u6548
veriCodeInvalid=\u9a8c\u8bc1\u7801\u8fc7\u671f<file_sep>/src/main/java/com/jeesun/thymelte/config/DruidConfiguration.java
package com.jeesun.thymelte.config;

import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

@Configuration
public class DruidConfiguration {
    @Autowired
    private Environment env;

    @Bean
    public ServletRegistrationBean statViewServlet(){
        ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
        //IP白名单
        bean.addInitParameter("allow", env.getProperty("druid.allow"));
        //IP黑名单（共同存在时，deny优先于allow）
        bean.addInitParameter("deny", env.getProperty("druid.deny"));
        //控制台管理用户
        bean.addInitParameter("loginUsername", env.getProperty("druid.login-username"));
        bean.addInitParameter("loginPassword", <PASSWORD>("<PASSWORD>"));
        //是否能够重置数据
        bean.addInitParameter("resetEnable", env.getProperty("druid.reset-enable"));

        return bean;
    }

    @Bean
    public FilterRegistrationBean statFilter(){
        FilterRegistrationBean bean = new FilterRegistrationBean(new WebStatFilter());
        //添加过滤规则
        bean.addUrlPatterns("/*");
        //忽略过滤的格式
        bean.addInitParameter("exclusions", "*.js,*.gif,*.svg,*.jpg,*.png,*.css,*.ico,/druid/*");
        return bean;
    }
}
<file_sep>/src/main/java/com/jeesun/thymelte/repository/UserInfoRepository.java
package com.jeesun.thymelte.repository;

import com.jeesun.thymelte.domain.UserInfo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
 * Created by simon on 2016/8/13.
 */

@Repository
public interface UserInfoRepository extends JpaRepository<UserInfo, Long> {
    UserInfo findById(Long id);
    UserInfo findByUserId(Long userId);
    UserInfo findByPhone(String phone);
    UserInfo findByUsername(String username);
    UserInfo findByEmail(String email);
}
<file_sep>/src/main/java/com/jeesun/thymelte/repository/LogLoginRepository.java
package com.jeesun.thymelte.repository;

import com.jeesun.thymelte.domain.LogLogin;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface LogLoginRepository extends JpaRepository<LogLogin, Long> {

}
<file_sep>/src/main/resources/static/js/register_form.js
/**
 *
 * User: simon
 * Date: 2018/07/08
 * Time: 10:01
 **/
$().ready(function() {
    //createCode();
    setTokenInHeader();

    jQuery.validator.addMethod("password", function (value, element, param) {
        var rex = /^(?![A-Z]+$)(?![a-z]+$)(?!\d+$)(?![\W_]+$)\S{6,16}$/;
        return this.optional(element) || (rex.test(value));
    }, "必须包含数字、字母或特殊字符");

    $("#registerForm").validate({
        debug: false,
        onkeyup: false,
        rules:{
            username: {
                required: true,
                minlength: 1,
                remote: {
                    type: 'POST',
                    url: 'users/checkExists',
                    data: {
                        username: function () {
                            return $('#username').val();
                        }
                    }
                }
            },
            password: {
                required: true,
                rangelength: [6,20],
                password: true
            },
            verifyPassword: {
                required: true,
                rangelength: [6,20],
                password: <PASSWORD>,
                equalTo: '#password'
            }
        },
        messages: {
            username:{
                required: "用户名必填",
                remote: "用户名已存在"
            },
            password: {
                required: '密码必填'
            },
            verifyPassword: {
                equalTo: '密码不相同'
            }
        },
        errorPlacement: function(error, element) {
            error.appendTo(element.parent());
        },
        errorElement: "span"
    });
});<file_sep>/src/main/resources/static/js/login.js
$(function(){
    $(".switchMode").on({
        click:function(e){
            if($(this).attr("src").indexOf('qrcode-40x40.png') != -1){
                //$(this).attr("src", "img/pc-40x40.png");
                //window.location.href = "login?qrcode=true";
                var qrcode = $.cookie('qrcode');
                console.log("qrcode=" + qrcode);
                if(qrcode == 'false' || !qrcode){
                    console.log("准备执行initQrCode");
                    $.cookie('qrcode', true);
                    initQrCode();
                }
                $('#form-qrcode').css('display', 'inline');
                $('#form-normal').css('display', 'none');
            }else {
                //$(this).attr("src", "img/qrcode-40x40.png");
                //window.location.href = "login";
                if(qrcode == true || !qrcode){
                    $.cookie('qrcode', false);
                }
                $('#form-qrcode').css('display', 'none');
                $('#form-normal').css('display', 'inline');
            }
        }
    });
});<file_sep>/src/main/java/com/jeesun/thymelte/domain/Authority.java
package com.jeesun.thymelte.domain;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;

/**
* @author SimonSun
* @create 2018-08-17 23:33:01
**/
@ApiModel(description = "Authority")
@Data
@Entity
@Table(name="authorities")
public class Authority implements GrantedAuthority, Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @ApiModelProperty(value = "user_id")
    @Column(name = "user_id", nullable = false)
    private Long userId;

    @ApiModelProperty(value = "authority")
    @Column(name = "authority", nullable = false)
    private String authority;
}<file_sep>/src/main/resources/static/js/login-form.js
//账号密码登录
$().ready(function() {
    //createCode();

    jQuery.validator.addMethod("password", function (value, element, param) {
        var rex = /^(?![A-Z]+$)(?![a-z]+$)(?!\d+$)(?![\W_]+$)\S{6,16}$/;
        return this.optional(element) || (rex.test(value));
    }, "必须包含数字、字母或特殊字符");

    jQuery.validator.addMethod("vericode", function (value, element, param) {
        return validate(value);
    }, "验证码错误");

    $("#loginForm").validate({
        rules:{
            username: {
                required: true,
                minlength: 1
            },
            password: {
                required: true,
                rangelength: [6,20],
                password: <PASSWORD>
            },
            input_vericode: {
                required: true,
                minlength: 4,
                vericode: true
            }
        },
        errorPlacement: function(error, element) {
            error.appendTo(element.parent());
        },
        errorElement: "span"
    });
});<file_sep>/src/main/resources/static/js/util.js
function getUrlParam(name) {
    var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
    var r = window.location.search.substr(1).match(reg);
    if (r != null)return decodeURI(r[2]);
    return null;
}

function getLocalTime(timestamp) {
    return new Date(parseInt(timestamp)).toLocaleString().replace(/:\d{1,2}$/, ' ');
}<file_sep>/src/main/resources/static/js/common.js
/**
 *
 * User: simon
 * Date: 2018/06/07
 * Time: 13:01
 **/
function setTokenInHeader() {
    var token = $("meta[name='_csrf']").attr("content");
    var header = $("meta[name='_csrf_header']").attr("content");
    $(document).ajaxSend(function (e, xhr, options) {
        xhr.setRequestHeader(header, token);
    });
}

//左侧菜单栏选中事件初始化
$('.sidebar-menu li').each(function () {
    $(this).removeClass("active");
});
var pathValue = window.location.href;
var pathName = pathValue.substring(pathValue.lastIndexOf('/') + 1);
$('.sidebar-menu li').each(function () {
    if(pathName == '' && $(this).children('a').attr('href').indexOf('index') != -1){
        $(this).addClass("active");
        $(this).parents(".treeview").addClass("active");
    }else if(pathName != ''&& $(this).children('a').attr('href').indexOf(pathName) != -1){
        $(this).addClass("active");
        $(this).parents(".treeview").addClass("active");
    }
});

/*var theme = $.cookie('theme');
console.log(theme);
if(!theme || null == theme){
    theme = 'blue';
    $.cookie('theme', theme);
}
changeTheme(theme);*/

function changeTheme(themeName) {
    /*$('body').removeClass('skin-blue skin-blue-light skin-green skin-green-light skin-red skin-red-light skin-black skin-black-light skin-purple skin-purple-light skin-yellow skin-yellow-light');*/
    $('body').removeClass($.cookie('theme'));
    $('body').addClass(themeName);
    $.cookie('theme', themeName, {expires: 30});
}<file_sep>/src/main/resources/static/js/news_info.js
/**
 *
 * User: simon
 * Date: 2018/06/06
 * Time: 19:23
 **/
$(function(){
    setTokenInHeader();

    initTable('table', {
        url: 'newsInfos/pageable',
        columns: [{
            field: 'state',
            checkbox: true
        },{
            field: 'title',
            title: '标题',
            align: 'center',
            searchable: true
        },{
            field: 'imageUrl',
            title: '缩略图',
            align: 'center',
            formatter: function (value, row, index, field) {
                return '<img src="' + value + '" />';
            }
        },{
            field: 'content',
            title: '内容',
            align: 'center',
            formatter: function (value, row, index, field) {
                //console.log(decodeURIComponent(value));
                return '<div style="max-width:600px; text-align: center">' + decodeURIComponent(value) + '</div>';
            }
        },{
            field: 'publishDate',
            title: '发布时间',
            align: 'center',
            formatter: function (value) {
                return new Date(value).format("yyyy-MM-dd hh:mm:ss");
            }
        }],
        ignoreColumn: [0, 2]
    });

    $('#commit_change').click(function () {
        $.ajax({
            cache: true,
            type: "POST",
            url:"newsInfos",
            data:$('#form_add').serialize(),// 你的form id
            async: true,
            error: function(request) {
                alert("Connection error:"+request.error);
            },
            success: function(data) {
                $('#myModal').modal('hide');
                showHint(data.message);
                setTimeout('hideHint()', 2000);
                refreshTable('table');
            }
        });
    });

    $('#plus').click(function () {
        $('#form_add input[name="id"]').val('');
        $('#form_add input[name="title"]').val('');
        $("#form_add .preview").empty();
        $('#file_paths').empty();
        $('#form_add input[name="content"]').val('');

        $('.preview').empty();
        $('.progress-bar').css('width', '0%');
        $('.proportion').html('');
        $('input[name="imageUrl"]').remove();

        $('#myModal').modal('show');
    });

    $('#edit').click(function () {
        var $select = $('#table').bootstrapTable('getSelections');
        if($select.length <= 0){
            alert('请至少选中一项');
        }else if($select.length > 1){
            alert('请选择一项');
        }else{
            $('#form_add input[name="id"]').val($select[0].id);
            $('#form_add input[name="title"]').val($select[0].title);
            $("#form_add .preview").empty();
            $("#form_add .preview").append('<div style="margin-top:10px;"><img src="' +$select[0].imageUrl + '"></div>');
            //$('#form_add input[name="content"]').val($select[0].content);//对froalaEditor无效
            $('textarea').froalaEditor('html.set', $select[0].content);
            //设置filePath
            $('#file_paths').empty();
            $('#file_paths').append('<input type="hidden" name="imageUrl" value="' + $select[0].imageUrl + '">');
            $('#myModal').modal('show');
        }
    });

    $('#minus').click(function () {
        var $select = $('#table').bootstrapTable('getSelections');
        if ($select.length <= 0) {
            alert('请至少选中一项');
        } else if ($select.length > 1) {
            var i;
            var ids = [];
            for(i = 0; i < $select.length; i++){
                ids[i] = $select[i].id;
            }
            $.get('newsInfos/delete/ids/' + ids.join(','), function(data){
                if(200 == data['code']){
                    showHint(data.message);
                    setTimeout('hideHint()', 2000);
                }
                refreshTable('table');
            });
        } else {
            $.get('newsInfos/delete/id/'+$select[0].id, function(data){
                if(200 == data['code']){
                    showHint(data.message);
                    setTimeout('hideHint()', 2000);
                }
                refreshTable('table');
            });
        }
    });
});<file_sep>/src/main/java/com/jeesun/thymelte/domain/NewsTag.java
package com.jeesun.thymelte.domain;

import com.simon.common.utils.SnowflakeGenId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.annotations.GenericGenerator;
import tk.mybatis.mapper.annotation.KeySql;

import javax.persistence.*;
import java.io.Serializable;

/**
* @author SimonSun
* @create 2018-08-18 00:41:52
**/
@ApiModel(description = "NewsTag")
@Data
@Entity
@Table(name="news_tag")
public class NewsTag implements Serializable{
    private static final long serialVersionUID = 1L;

    @Id
    @KeySql(genId = SnowflakeGenId.class)
    @GeneratedValue(generator = "sequenceId")
    @GenericGenerator(name = "sequenceId", strategy = "com.simon.common.utils.snowflake.SequenceId")
    private Long id;

    @ApiModelProperty(value = "news_info_id")
    @Column(name = "news_info_id")
    private Long newsInfoId;

    @ApiModelProperty(value = "tag_id")
    @Column(name = "tag_id")
    private Long tagId;
}<file_sep>/src/main/java/com/jeesun/thymelte/domain/LogLogin.java
package com.jeesun.thymelte.domain;

import com.simon.common.utils.SnowflakeGenId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.annotations.GenericGenerator;
import tk.mybatis.mapper.annotation.KeySql;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

/**
* @author SimonSun
* @create 2018-08-17 23:33:01
**/
@ApiModel(description = "LogLogin")
@Data
@Entity
@Table(name="log_login")
public class LogLogin implements Serializable{
    private static final long serialVersionUID = 1L;

    @Id
    @KeySql(genId = SnowflakeGenId.class)
    @GeneratedValue(generator = "sequenceId")
    @GenericGenerator(name = "sequenceId", strategy = "com.simon.common.utils.snowflake.SequenceId")
    private Long id;

    @ApiModelProperty(value = "create_time")
    @Column(name = "create_time")
    private Date createTime;

    @ApiModelProperty(value = "ip")
    @Column(name = "ip")
    private String ip;

    @ApiModelProperty(value = "username")
    @Column(name = "username")
    private String username;
}<file_sep>/src/main/java/com/jeesun/thymelte/custom/ErrorPageInterceptor.java
package com.jeesun.thymelte.custom;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;

/**
 * 错误页面拦截器
 *
 * @author simon
 * @create 2018-06-13 1:39
 **/

@Component
public class ErrorPageInterceptor extends HandlerInterceptorAdapter {
    private List<Integer> errorCodeList = Arrays.asList(404, 403, 500, 501);
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws
            Exception {
        if (errorCodeList.contains(response.getStatus())) {
            response.sendRedirect("error/" + response.getStatus());
            return false;
        }
        return super.preHandle(request, response, handler);
    }
}
<file_sep>/src/main/java/com/jeesun/thymelte/controller/UploadFileController.java
package com.jeesun.thymelte.controller;

import com.jeesun.thymelte.domain.ResultMsg;
import com.jeesun.thymelte.util.FileUploadUtil;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

/**
 * 上传文件
 *
 * @author simon
 * @create 2018-06-06 2:12
 **/
@Controller
@RequestMapping("uploadFiles")
public class UploadFileController {
    private static Logger logger = Logger.getLogger(UploadFileController.class);

    private final ResourceLoader resourceLoader;

    private final String ROOT = "upload";

    @Autowired
    public UploadFileController(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
        if(!Files.exists(Paths.get(ROOT))){
            try{
                Files.createDirectories(Paths.get(ROOT));
            }catch (IOException e){
                logger.error(e);
            }
        }
    }

    @RequestMapping(value = "upload", method = RequestMethod.POST)
    @ResponseBody
    public ResultMsg post(@RequestParam("file") MultipartFile[] files){
        String[] savedFiles = FileUploadUtil.saveFiles(files);
        if(null == savedFiles || savedFiles.length <= 0){
            return new ResultMsg(404, "保存文件失败", null);
        }else{
            return new ResultMsg(200, "保存文件成功", savedFiles);
        }
    }

    @RequestMapping(value = "uploadFile", method = RequestMethod.POST)
    @ResponseBody
    public Map<String, Object> uploadTest(@RequestParam("file") MultipartFile[] files){
        Map<String, Object> resultMap = new HashMap<>();
        String[] savedFiles = FileUploadUtil.saveFiles(files);
        if(null == savedFiles || savedFiles.length <= 0){
            resultMap.put("link", null);
        }else{
            StringBuffer filePath = new StringBuffer();
            filePath.append("uploadFiles");
            filePath.append("/file/");
            filePath.append(savedFiles[0]);
            resultMap.put("link", filePath);
        }

        return resultMap;
    }

    @RequestMapping(value = "/file/**", method = RequestMethod.GET)
    @ResponseBody
    public ResponseEntity<?> getFile(HttpServletRequest request, HttpServletResponse response) {
        String filePath = request.getRequestURI();
        //logger.info(filePath);
        filePath = filePath.substring(filePath.indexOf("uploadFiles/file/") + "uploadFiles/file/".length());
        //logger.info(filePath);
        String fileRoot = filePath.substring(0, filePath.lastIndexOf("/"));
        //logger.info(fileRoot);
        String fileName = filePath.substring(filePath.lastIndexOf("/") + 1);
        //logger.info(fileName);
        try {
            return ResponseEntity.ok(resourceLoader.getResource("file:" + Paths.get(fileRoot, fileName).toString()));
        } catch (Exception e) {
            return ResponseEntity.notFound().build();
        }
    }
}
<file_sep>/src/main/resources/static/js/vcode.js
var code = "";
$().ready(function() {
    //将函数返回值赋给code
    code = createCode();

    //点击canvas图片更换验证码
    $("#codeimg").click(function () {
        code = createCode();
    });

    /*随机字符函数*/
    function rand(){
        //去掉i,I,l,o,O等易混淆字母
        var str="abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ0123456789";
        //将字符串分隔为数组
        var arr=str.split("");
        //随机字符在[0,56]之间
        var ranNum=Math.floor(Math.random()*57);
        var captcha=arr[ranNum];
        return captcha;
    }

    /*随机干扰线条函数*/
    function drawline(canvas, context) {
        //若省略beginPath，则每点击一次验证码会累积干扰线的条数
        context.beginPath();
        //起点与终点在canvas宽高内随机
        context.moveTo(Math.floor(Math.random() * canvas.width), Math.floor(Math.random() * canvas.height));
        context.lineTo(Math.floor(Math.random() * canvas.width), Math.floor(Math.random() * canvas.height));
        context.lineWidth = 1;
        context.strokeStyle = '#275DB3';
        context.stroke();
    }

    /*生成验证码*/
    function createCode(){
        //每次生成code先将其清空防止叠加
        code = "";
        var canvas = document.getElementById("codeimg");
        if(!canvas){
            return null;
        }
        var context = canvas.getContext("2d");

        //清空画布
        context.clearRect(0, 0, canvas.width, canvas.height);

        context.strokeStyle = "#FFF";
        context.strokeRect(0, 0, canvas.width, canvas.height);

        //生成干扰线，数量随意
        for (var i = 0; i < 10; i++) {
            drawline(canvas, context);
        }

        //循环生成4位验证码
        for (var k = 0; k < 4; k++) {
            context.font='76px Arial';
            //将初始状态保存
            context.save();
            //获得-1到1的随机数
            var rA = 1-Math.random()*2;
            //获取随机倾斜角
            var angle = rA / 8 ;
            var ranNum = rand();
            //旋转生成的随机字符
            context.rotate(angle);
            //把rand()生成的随机数文本依次填充到canvas中，注意x坐标
            context.fillText(ranNum,20+45*k,100);
            //恢复初始状态，以便下一次循环
            context.restore();
            code += ranNum;
        }
        //返回生成的验证码字符串
        return code;
    }
});

function validate(value) {
    return value.toUpperCase() === code.toUpperCase();
}<file_sep>/src/main/resources/messages_en_US.properties
usernameNotFound=username not found
accountDisabled=account was suspended
passwordError=<PASSWORD>
tokenWasNotRecognised=Token was not recognised
helloWorld=hello, world
invalidToken=invalid token
approve=Approve
deny=Deny
authorize=authorize
appApproval=App Approval
doYouAuthorize=Do you authorize app named 
toAccessYouProtectedResources=to access your protected resources?
login=login
signIn=sign in
username=username
password=<PASSWORD>
error=error
userNotValid=user not valid
veriCodeInvalid=verification code is invalid