SpringSecurity 集成thymeleaf和图片验证码

SpringSecurity 集成thymeleaf

当用户没有某权限时,页面不展示该按钮

添加依赖:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<scope>runtime</scope>
</dependency>
<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
	<version>2.2.2</version>
</dependency>
<dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
	<groupId>org.thymeleaf.extras</groupId>
	<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

创建application.yml:

spring:
  # 数据源
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/security_study?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 1234
  thymeleaf:
    cache: false  # 开发时可以不使用缓存, 有时候页面改了直接重构, 页面就可以更改, 可以不用重启项目
    check-template: true # 是否检查模板(前端thymeleaf页面) , 检查

#mybatis配置
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.powernode.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

新建启动类

package com.powernode;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.powernode.dao")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

配置实体类

package com.powernode.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SysUser implements Serializable {
    private Integer userId;
    private String username;
    private String password;
    private String sex;
    private String address;
    private Integer enabled;
    private Integer accountNoExpired;
    private Integer credentialsNoExpired;
    private Integer accountNoLocked;
}
package com.powernode.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SysMenu implements Serializable {
    private Integer id;
    private Integer pid;
    private Integer type;
    private String name;
    private String code;
}

配置mapper

package com.powernode.dao;

import com.powernode.entity.SysUser;
import org.apache.ibatis.annotations.Param;

public interface SysUserDao {
    /**
     * 根据用户名获取用户信息
     *
     * @param name
     * @return
     */
    // @Param("userName"): 和参数对应, 形参可以随便命名, 但是注解中的名字是实体类中的属性名,
    //      在xml文件中会使用到这个实体类中的属性, 如果不写注解, 就需要两个的名字相同
    SysUser getByUserName(@Param("userName") String name);
}
package com.powernode.dao;

import org.apache.ibatis.annotations.Param;

import java.util.List;

public interface SysMenuDao {
    List<String> queryPermissionsByUserId(@Param("userId") Integer userId);
}

配置映射文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.powernode.dao.SysUserDao">
    <select id="getByUserName" resultType="sysUser">
        select user_id,
               username,
               password,
               sex,
               address,
               enabled,
               account_no_expired,
               credentials_no_expired,
               account_no_locked
        from sys_user
        where username = #{userName}
    </select>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.powernode.dao.SysMenuDao">
    <select id="queryPermissionsByUserId" resultType="string">
        select distinct sm.code
        from sys_role_user sru
                 inner join sys_role_menu srm on sru.rid = srm.rid
                 inner join sys_menu sm on srm.mid = sm.id
        where sru.uid = #{userId}
    </select>
</mapper>

新建安全用户类(实现UserDetails 接口):

package com.powernode.vo;

import com.powernode.entity.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

public class MySecurityUser implements UserDetails {
    private final SysUser sysUser;
    //用于存储权限的list
    private List<SimpleGrantedAuthority> authorityList;
    public MySecurityUser(SysUser sysUser){
        this.sysUser=sysUser;
    }

    /**
     * 返回用户所拥有的权限
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorityList;
    }

    public void setAuthorityList(List<SimpleGrantedAuthority> authorityList) {
        this.authorityList = authorityList;
    }

    @Override
    public String getPassword() {
        String myPassword=sysUser.getPassword();
        sysUser.setPassword(null); //擦除我们的密码,防止传到前端
        return myPassword;
    }

    @Override
    public String getUsername() {
        return this.sysUser.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return sysUser.getAccountNoExpired().equals(1);
    }

    @Override
    public boolean isAccountNonLocked() {
        return sysUser.getAccountNoLocked().equals(1);
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return sysUser.getCredentialsNoExpired().equals(1);
    }

    @Override
    public boolean isEnabled() {
        return sysUser.getEnabled().equals(1);
    }
}

新建service,根据用户名获取用户信息:

package com.powernode.service;

import com.powernode.entity.SysUser;
import org.apache.ibatis.annotations.Param;

public interface SysUserService {
    /**
     * 根据用户名获取用户信息
     * @param name
     * @return
     */
    SysUser getByUserName(String name);
}
package com.powernode.service;

import org.apache.ibatis.annotations.Param;

import java.util.List;

public interface SysMenuService {
    List<String> queryPermissionsByUserId(Integer userId);
}

新建serviceImpl实现service接口:

package com.powernode.service.impl;

import com.powernode.dao.SysUserDao;
import com.powernode.entity.SysUser;
import com.powernode.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
@Slf4j
public class SysUserServiceImpl implements SysUserService {
    @Resource
    private SysUserDao sysUserDao;
    @Override
    public SysUser getByUserName(String userName) {
        return sysUserDao.getByUserName(userName);
    }
}
package com.powernode.service.impl;

import com.powernode.dao.SysMenuDao;
import com.powernode.service.SysMenuService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

@Service
@Slf4j
public class SysMenuServiceImpl implements SysMenuService {
    @Resource
    private SysMenuDao sysMenuDao;
    @Override
    public List<String> queryPermissionsByUserId(Integer userId) {
        return sysMenuDao.queryPermissionsByUserId(userId);
    }
}

新建SecurityUserDetailsServiceImpl 实现UserDetailService接口:

package com.powernode.service.impl;

import com.powernode.entity.SysUser;
import com.powernode.service.SysMenuService;
import com.powernode.service.SysUserService;
import com.powernode.vo.MySecurityUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Service
@Slf4j
public class SecurityUserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private SysUserService sysUserService;
    @Resource
    private SysMenuService sysMenuService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = sysUserService.getByUserName(username);
        if (null == sysUser) {
            throw new UsernameNotFoundException("该用户不存在");
        }
        // 根据用户id获取该用户所拥有的权限,List<SimpleGrantedAuthority>
        List<String> userPermissions = sysMenuService.queryPermissionsByUserId(sysUser.getUserId());

        // 遍历权限,把权限放到列表中
        List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
        for (String userPermission : userPermissions) {
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(userPermission);
            authorityList.add(simpleGrantedAuthority);
        }

        List<SimpleGrantedAuthority> authorityList1 = userPermissions.stream()
                /*.map(userPermission -> {
                    SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(userPermission);
                    return simpleGrantedAuthority;
                })*/
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());


        MySecurityUser securityUser = new MySecurityUser(sysUser);
        securityUser.setAuthorityList(authorityList);
        return securityUser;
    }
}

创建测试的Studentcontroller、TeacherController:

package com.powernode.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Controller
@Slf4j
@RequestMapping("/student")
public class StudentController {

    @GetMapping("/query")
    @PreAuthorize("hasAuthority('student:query')")
    public String queryInfo(){
        //  /templates/student/query.html
        return "student/query";
    }

    @GetMapping("/add")
    @PreAuthorize("hasAuthority('student:add')")
    public String addInfo(){
        return "student/add";
    }

    @GetMapping("/update")
    @PreAuthorize("hasAuthority('student:update')")
    public String updateInfo(){
        return "student/update";
    }

    @GetMapping("/delete")
    @PreAuthorize("hasAuthority('student:delete')")
    public String deleteInfo(){
        return "student/delete";
    }

    @GetMapping("/export")
    @PreAuthorize("hasAuthority('student:export')")
    public String exportInfo(){
        return "student/export";
    }
}
package com.powernode.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
@RequestMapping("/teacher")
public class TeacherController {
    @GetMapping("/query")
    @PreAuthorize("hasAuthority('teacher:query')") //预授权
    public String queryInfo(){
        return "I am a teacher!";
    }
}

新建LoginController和IndexController:

package com.powernode.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller // 这里不返回json字符串, 返回一个thymeleaf模板
@Slf4j
public class LoginController {
    @RequestMapping("/toLogin")
    public String toLogin() {
        // 返回thymeleaf的逻辑视图名,物理视图=前缀+逻辑视图名+后缀
        //    /templates/ + login + .html(默认前缀 加 逻辑视图 加 默认的后缀)

        // 返回thymeleaf的逻辑视图
        return "login";
    }
}
package com.powernode.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@Slf4j
@RequestMapping("/index")
public class IndexController {
    @RequestMapping("/toIndex")
    public String toIndex(){
        return "main";
    }
}

创建安全配置文件WebSecurityConfig:

package com.powernode.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;


@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 所有请求,都需要认证
        http.authorizeRequests().anyRequest().authenticated();

        // 这个是排除了的, 不需要登录就可以访问的
        http.formLogin()
                .loginPage("/toLogin") // 配置登录页面
                .usernameParameter("uname") // 用户名参数
                .passwordParameter("pwd") // 密码参数
                .loginProcessingUrl("/login/doLogin") // 单击登录后进入url
                .failureForwardUrl("/toLogin") // 登录失败
                .successForwardUrl("/index/toIndex") // 登录成功
                .permitAll(); // 配置登录

        http.logout().logoutSuccessUrl("/toLogin"); // 配置退出成功登录页面

        // 跨域请求(要携带token, 防止被攻击, 这里没有token, 取消了保护)
        http.csrf().disable(); // 关闭跨域请求保护
    }
}

创建静态页面login.html和main.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
    <meta charset="UTF-8">
    <title>用户登陆</title>
</head>
<body>
<h2>登录页面</h2>
<form action="/login/doLogin" method="post">
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" name="uname" value="thomas"></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="password" name="pwd"></td>
            <span th:if="${param.error}">用户名或者密码错误</span>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit">登录</button>
            </td>
        </tr>
    </table>
</form>
</body>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>系统首页</title>
</head>
<body>
<h1 align="center">系统首页</h1>
<a href="/student/query" sec:authorize="hasAuthority('student:query')">查询学生</a>
<br>
<a href="/student/add" sec:authorize="hasAuthority('student:add')">添加学生</a>
<br>
<a href="/student/update" sec:authorize="hasAuthority('student:update')">更新学生</a>
<br>
<a href="/student/delete" sec:authorize="hasAuthority('student:delete')">删除学生</a>
<br>
<a href="/student/export" sec:authorize="hasAuthority('student:export')">导出学生</a>
<br>
<br><br><br>
<h2><a href="/logout">退出</a></h2>
<br>
</body>
</html>

在templates/student下面创建学生管理的各个页面:

创建export.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
>
<head>
    <meta charset="UTF-8">
    <title>系统首页-学生管理</title>
</head>
<body>
<h1 align="center">系统首页-学生管理-导出</h1>
<a href="/index/toIndex">返回</a>
<br>
</body>
</html>

创建query.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>系统首页-学生管理</title>
</head>
<body>
<h1 align="center">系统首页-学生管理-查询</h1>
<a href="/index/toIndex">返回</a>
<br>
</body>
</html>

创建add.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>系统首页-学生管理</title>
</head>
<body>
<h1 align="center">系统首页-学生管理-新增</h1>
<a href="/index/toIndex">返回</a>
<br>
</body>
</html>

创建update.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>系统首页-学生管理</title>
</head>
<body>
<h1 align="center">系统首页-学生管理-更新</h1>
<a href="/index/toIndex">返回</a>
<br>
</body>
</html>

创建delete.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>系统首页-学生管理</title>
</head>
<body>
<h1 align="center">系统首页-学生管理-删除</h1>
<a href="/index/toIndex">返回</a>
<br>
</body>
</html>

创建403页面:

在static/error下面创建403.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>403</title>
</head>
<body>
<h2>403:您没有权限访问此页面</h2>
<a href="/index/toIndex">去首页</a>
</body>
</html>

启动测试:

登录用户
没有权限

集成图片验证码(集成thymeleaf基础上添加)

Spring Security是通过过滤器链来完成了,所以它的解决方案是创建一个过滤器放到Security的过滤器链中,在自定义的过滤器中比较验证码

添加依赖(用于生成验证码)

<!--引入hutool:可以生成验证码-->
<dependency>
	<groupId>cn.hutool</groupId>
	<artifactId>hutool-all</artifactId>
	<version>5.3.9</version>
</dependency>

添加一个获取验证码的接口:

package com.powernode.controller;

import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;

@Controller
@Slf4j
public class CaptchaController {
    /**
     * 获取图片验证码
     */
    @GetMapping("/code/image")
    public void getCaptchaCode(HttpServletRequest request,HttpServletResponse response) throws IOException {
        CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(200, 100, 2, 20);
        String code = circleCaptcha.getCode();
        log.info("生成的图片验证码为:{}",code);
        //将验证码存储到session中
        request.getSession().setAttribute("CAPTCHA_CODE",code);
        //将图片写到响应流里,参数一,图片,参数2:图片格式,参数3:响应流
        ImageIO.write(circleCaptcha.getImage(),"JPEG",response.getOutputStream());
    }
}

创建验证码过滤器:

package com.powernode.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.thymeleaf.util.StringUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
@Slf4j
public class ValidateCodeFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // TODO 判断路径 是否是/login/doLogin
        String requestURI = request.getRequestURI();
        if(!requestURI.equals("/login/doLogin")){// 不是登录请求,直接放行
            doFilter(request,response,filterChain); //直接下一个
            return;
        }
        //校验验证码
        validateCode(request,response,filterChain);

    }

    private void validateCode(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        //1 从前端获取用户输入的验证码
        String enterCode = request.getParameter("code");
        //2 从session中获取验证码
        String captchaCodeInSession = (String) request.getSession().getAttribute("CAPTCHA_CODE");
        request.getSession().removeAttribute("captcha_code_error"); //清除提示信息
        if(StringUtils.isEmpty(enterCode)){
            request.getSession().setAttribute("captcha_code_error","请输入验证码");
            response.sendRedirect("/toLogin");
            return;
        }
        if(StringUtils.isEmpty(captchaCodeInSession)){
            request.getSession().setAttribute("captcha_code_error","验证码错误");
            response.sendRedirect("/toLogin");
            return;
        }
        //3 判断二者是否相等
        if(!enterCode.equalsIgnoreCase(captchaCodeInSession)){
            request.getSession().setAttribute("captcha_code_error","验证码输入错误");
            response.sendRedirect("/toLogin");
            return;
        }
        request.getSession().removeAttribute("CAPTCHA_CODE"); //删除session中的验证码
        //如果程序执行到这里,说明验证码正确,放行
        this.doFilter(request,response,filterChain);
    }
}

修改WebSecurityConfig(重点):

package com.powernode.config;

import com.powernode.filter.ValidateCodeFilter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;


@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private ValidateCodeFilter validateCodeFilter;

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //在用户名密码认证过滤器前添加图片验证码过滤器
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
        //所有请求,都需要认证
        http.authorizeRequests()
                .mvcMatchers("/code/image")
                .permitAll() //放开验证码的请求
                .anyRequest().authenticated();
        http.formLogin()
                .loginPage("/toLogin") //配置登录页面
                .usernameParameter("uname") //用户名参数
                .passwordParameter("pwd") //密码参数
                .loginProcessingUrl("/login/doLogin") //单击登录后进入url
                .failureForwardUrl("/toLogin") //登录失败
                .successForwardUrl("/index/toIndex") //登录成功
                .permitAll(); //配置登录
        http.logout().logoutSuccessUrl("/toLogin"); //配置退出成功登录页面

        http.csrf().disable(); //关闭跨域请求保护
    }
}

修改login.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>用户登陆</title>
</head>
<body>
<h2>登录页面</h2>
<form action="/login/doLogin" method="post">
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" name="uname" value="thomas"></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="password" name="pwd"></td>
            <span th:if="${param.error}">用户名或者密码错误</span>
        </tr>
        <tr>
            <td>验证码:</td>
            <td>
                <input type="text" name="code"/>
                <img src="/code/image" style="height:33px;cursor:pointer;" onclick="this.src=this.src"/>
                <span th:text="${session.captcha_code_error}" style="color: #FF0000;">username</span>
            </td>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit">登录</button>
            </td>
        </tr>
    </table>
</form>
</body>

测试登录:

加入验证码

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值