添加依赖
<!--验证码的实现依赖 https://mvnrepository.com/artifact/com.github.penggle/kaptcha -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
编写验证码的配置
/**
* 验证码配置
*
* @author ruoyi
*/
@Configuration
public class CaptchaConfig {
@Bean(name = "captchaProducer")
public DefaultKaptcha getKaptchaBean() {
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// 是否有边框 默认为true 我们可以自己设置yes,no
properties.setProperty(KAPTCHA_BORDER, "yes");
// 验证码文本字符颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
// 验证码图片宽度 默认为200
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
// 验证码图片高度 默认为50
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
// 验证码文本字符大小 默认为40
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
// KAPTCHA_SESSION_KEY
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
// 验证码文本字符长度 默认为5
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google
// .code.kaptcha.impl.ShadowGimpy
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
@Bean(name = "captchaProducerMath")
public DefaultKaptcha getKaptchaBeanMath() {
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// 是否有边框 默认为true 我们可以自己设置yes,no
properties.setProperty(KAPTCHA_BORDER, "yes");
// 边框颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");
// 验证码文本字符颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
// 验证码图片宽度 默认为200
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
// 验证码图片高度 默认为50
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
// 验证码文本字符大小 默认为40
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");
// KAPTCHA_SESSION_KEY
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
// 验证码文本生成器,这个为自己本地的,全路径一定要写对
properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.demo.springboot.shiro.filter.KaptchaTextCreator");
// 验证码文本字符间距 默认为2
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
// 验证码文本字符长度 默认为5
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
// 验证码噪点颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
// 干扰实现类
properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google
// .code.kaptcha.impl.ShadowGimpy
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
编写验证码文本生成器
/**
* 验证码文本生成器
*
* @author ruoyi
*/
public class KaptchaTextCreator extends DefaultTextCreator {
private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(",");
@Override
public String getText() {
int result = 0;
Random random = new SecureRandom();
int x = random.nextInt(10);
int y = random.nextInt(10);
StringBuilder suChinese = new StringBuilder();
int randomoperands = (int) Math.round(Math.random() * 2);
if (randomoperands == 0) {
result = x * y;
suChinese.append(CNUMBERS[x]);
suChinese.append("*");
suChinese.append(CNUMBERS[y]);
} else if (randomoperands == 1) {
if (x != 0 && y % x == 0) {
result = y / x;
suChinese.append(CNUMBERS[y]);
suChinese.append("/");
suChinese.append(CNUMBERS[x]);
} else {
result = x + y;
suChinese.append(CNUMBERS[x]);
suChinese.append("+");
suChinese.append(CNUMBERS[y]);
}
} else if (randomoperands == 2) {
if (x >= y) {
result = x - y;
suChinese.append(CNUMBERS[x]);
suChinese.append("-");
suChinese.append(CNUMBERS[y]);
} else {
result = y - x;
suChinese.append(CNUMBERS[y]);
suChinese.append("-");
suChinese.append(CNUMBERS[x]);
}
} else {
result = x + y;
suChinese.append(CNUMBERS[x]);
suChinese.append("+");
suChinese.append(CNUMBERS[y]);
}
suChinese.append("=?@").append(result);
return suChinese.toString();
}
}
编写验证码过滤器
/**
* 验证码过滤器
*
* @author ruoyi
*/
public class CaptchaValidateFilter extends AccessControlFilter {
/**
* 是否开启验证码
*/
private boolean captchaEnabled = true;
/**
* 验证码类型
*/
private String captchaType = "math";
public void setCaptchaEnabled(boolean captchaEnabled) {
this.captchaEnabled = captchaEnabled;
}
public void setCaptchaType(String captchaType) {
this.captchaType = captchaType;
}
@Override
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
request.setAttribute("captchaEnabled", captchaEnabled);
request.setAttribute("captchaType", captchaType);
return super.onPreHandle(request, response, mappedValue);
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// 验证码禁用 或不是表单提交 允许访问
if (!captchaEnabled || !"post".equals(httpServletRequest.getMethod().toLowerCase())) {
return true;
}
return validateResponse(httpServletRequest.getParameter("validateCode"));
}
public boolean validateResponse(String validateCode) {
Object obj = SecurityUtils.getSubject().getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);
String code = String.valueOf(obj != null ? obj : "");
return !StringUtils.isEmpty(validateCode) && validateCode.equalsIgnoreCase(code);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
request.setAttribute("captcha", "captchaError");
return true;
}
}
验证码过滤器需要在ShiroConfig配置中进行配置
/**
* 自定义验证码过滤器
*/
@Bean
public CaptchaValidateFilter captchaValidateFilter() {
CaptchaValidateFilter captchaValidateFilter = new CaptchaValidateFilter();
captchaValidateFilter.setCaptchaEnabled(true);
captchaValidateFilter.setCaptchaType("math");
return captchaValidateFilter;
}
/**
* Shiro过滤器配置
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// Shiro的核心安全接口,这个属性是必须的
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 身份认证失败,则跳转到登录页面的配置
shiroFilterFactoryBean.setLoginUrl("/");
// 权限认证失败,则跳转到指定页面
shiroFilterFactoryBean.setUnauthorizedUrl("/path/unauth");
// Shiro连接约束配置,即过滤链的定义
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
/**
* 对静态资源设置匿名访问,
* anon:对应的拦截器不会进行拦截
* authc:必须认证才可以访问
* user:如果使用rememberMe的功能可以直接访问
* perms:该资源必须得到资源权限才可以访问
* role:该资源必须得到角色权限才可以访问
*/
filterChainDefinitionMap.put("/favicon.ico**", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/h-ui.admin/**", "anon");
filterChainDefinitionMap.put("/fonts/**", "anon");
filterChainDefinitionMap.put("/images/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/h-ui/**", "anon");
filterChainDefinitionMap.put("/user/login", "anon,captchaValidate");
filterChainDefinitionMap.put("/user/register", "anon,captchaValidate");
filterChainDefinitionMap.put("/path/toPage/register", "anon");
filterChainDefinitionMap.put("/error/**","anon");
filterChainDefinitionMap.put("/lib/**","anon");
filterChainDefinitionMap.put("/","anon,captchaValidate");
filterChainDefinitionMap.put("/path/unauth","anon");
filterChainDefinitionMap.put("/temp/**","anon");
filterChainDefinitionMap.put("/captcha/captchaImage**", "anon");
// // 退出 logout地址,shiro去清除session
// filterChainDefinitionMap.put("/logout", "logout");
Map<String, Filter> filters = new LinkedHashMap<>();
filters.put("captchaValidate", captchaValidateFilter());
shiroFilterFactoryBean.setFilters(filters);
//其他资源都需要认证 authc 表示需要认证才能进行访问 user表示配置记住我或认证通过可以访问的地址,
// 必须放到最后面,因为过滤是有顺序的
filterChainDefinitionMap.put("/**", "user");
//设置一个拦截器链
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
添加生成图片验证码并输出
/**
* 图片验证码(支持算术形式)
*
* @author ruoyi
*/
@Controller
@RequestMapping("/captcha")
public class CaptchaController{
@Resource(name = "captchaProducer")
private Producer captchaProducer;
@Resource(name = "captchaProducerMath")
private Producer captchaProducerMath;
/**
* 验证码生成
*/
@GetMapping(value = "/captchaImage")
public ModelAndView getKaptchaImage(HttpServletRequest request, HttpServletResponse response) {
ServletOutputStream out = null;
try {
HttpSession session = request.getSession();
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
response.setHeader("Pragma", "no-cache");
response.setContentType("image/jpeg");
String type = request.getParameter("type");
String capStr;
String code = null;
BufferedImage bi = null;
if ("math".equals(type)) {
String capText = captchaProducerMath.createText();
capStr = capText.substring(0, capText.lastIndexOf("@"));
code = capText.substring(capText.lastIndexOf("@") + 1);
bi = captchaProducerMath.createImage(capStr);
} else if ("char".equals(type)) {
capStr = code = captchaProducer.createText();
bi = captchaProducer.createImage(capStr);
}
System.out.println("验证码的值"+code);
session.setAttribute(Constants.KAPTCHA_SESSION_KEY, code);
out = response.getOutputStream();
assert bi != null;
ImageIO.write(bi, "jpg", out);
out.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
在登录功能中的判断
/**
* 登录功能
*/
@PostMapping("/login")
public ModelAndView login(@RequestParam("username") String username,
@RequestParam("password") String password,
@RequestParam("validateCode") String validateCode,
@RequestParam(value = "online", required = false) String rememberMeValue,
ModelAndView modelAndView) {
boolean rememberMe = false;
if (rememberMeValue != null) {
rememberMe = true;
}
LOGGER.info("rememberMe:{}", rememberMe);
//使用shiro编写认证操作
// 1.获取Subject
Subject subject = SecurityUtils.getSubject();
// 2.封装用户数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
// 3.执行登录方法
try {
subject.login(token);
modelAndView.setViewName("index");
} catch (UnknownAccountException e) {
modelAndView.addObject("msg", "用户名不存在");
modelAndView.setViewName("login");
} catch (IncorrectCredentialsException e) {
modelAndView.addObject("msg", "密码错误");
modelAndView.setViewName("login");
}catch (CaptchaException e){
throw new AuthenticationException(e.getMessage(), e);
}
//判断验证码
Session session =subject.getSession();
String code = (String) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
LOGGER.info("code:{}",code);
if (!(code).equals(validateCode)){
modelAndView.addObject("msg", "验证码错误!");
modelAndView.setViewName("login");
}
return modelAndView;
}
HTML中的编写
<div class="formControls col-xs-8 col-xs-offset-3">
<input class="input-text size-L" type="text" placeholder="验证码" name="validateCode" style="width:150px;" maxlength="5">
<a id="kanbuq" href="javascript:void(0);" title="点击更换验证码">
<img th:src="@{/captcha/captchaImage(type=${captchaType})}" id="imgcode">
</a>
</div>
获取验证码类型和相对路径
<script th:inline="javascript"> var ctx = [[@{/}]];var captchaType = [[${captchaType}]];</script>
点击切换验证码
$(function() {
$('#imgcode').click(function() {
var url = ctx + "captcha/captchaImage?type=" + captchaType + "&s=" + Math.random();
$("#imgcode").attr("src", url);
});
});
参考网站,参考的若依后台框架。