概述

  基于jwt的token认证方案

验证码

  框架的搭建,可以自己根据网上搭建,或者看我博客springboot相关的博客,这边就不做介绍了。验证码生成可以利用Java第三方组件,引入

       <dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>

配置验证码相关的属性

@Component
public class KaptchaConfig
{
@Bean
public DefaultKaptcha getDefaultKaptcha()
{
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
/*是否使用边框*/
properties.setProperty("kaptcha.border","no");
/*验证码 边框颜色*/
//properties.setProperty("kaptcha.border.color","black");
/*验证码干扰线 颜色*/
properties.setProperty("kaptcha.noise.color","black");
/*验证码宽度*/
properties.setProperty("kaptcha.image.width","110");
/*验证码高度*/
properties.setProperty("kaptcha.image.height","40");
//properties.setProperty("kaptcha.session.key","code");
/*验证码颜色*/
properties.setProperty("kaptcha.textproducer.font.color","204,128,255");
/*验证码大小*/
properties.setProperty("kaptcha.textproducer.font.size","30");
properties.setProperty("kaptcha.textproducer.char.space","3");
/*验证码字数*/
properties.setProperty("kaptcha.textproducer.char.length","4");
/*验证码 背景渐变色 开始*/
properties.setProperty("kaptcha.background.clear.from","240,240,240");
/*验证码渐变色 结束*/
properties.setProperty("kaptcha.background.clear.to","240,240,240");
/*验证码字体*/
properties.setProperty("kaptcha.textproducer.font.names", "Arial,微软雅黑");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}

配置相应的配置接口就能生成验证码,但是这钟样式有点不好看,如果自定义还非常麻烦,索性

利用网上大佬写好的工具类(链接不见了,找到在加上)

import javax.imageio.ImageIO;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.LinearGradientPaint;
import java.awt.Paint;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Random; /**
*
* Description:验证码工具类
* @author huangweicheng
* @date 2019/10/23
*/
public class VerifyCodeUtils
{
//使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
private static Random random = new Random(); /**
* 使用系统默认字符源生成验证码
* @param verifySize 验证码长度
* @return
*/
public static String generateVerifyCode(int verifySize){
return generateVerifyCode(verifySize, VERIFY_CODES);
}
/**
* 使用指定源生成验证码
* @param verifySize 验证码长度
* @param sources 验证码字符源
* @return
*/
public static String generateVerifyCode(int verifySize, String sources){
if(sources == null || sources.length() == 0){
sources = VERIFY_CODES;
}
int codesLen = sources.length();
Random rand = new Random(System.currentTimeMillis());
StringBuilder verifyCode = new StringBuilder(verifySize);
for(int i = 0; i < verifySize; i++){
verifyCode.append(sources.charAt(rand.nextInt(codesLen-1)));
}
return verifyCode.toString();
} /**
* 生成随机验证码文件,并返回验证码值
* @param w
* @param h
* @param outputFile
* @param verifySize
* @return
* @throws IOException
*/
public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException{
String verifyCode = generateVerifyCode(verifySize);
outputImage(w, h, outputFile, verifyCode);
return verifyCode;
} /**
* 输出随机验证码图片流,并返回验证码值
* @param w
* @param h
* @param os
* @param verifySize
* @return
* @throws IOException
*/
public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException{
String verifyCode = generateVerifyCode(verifySize);
outputImage(w, h, os, verifyCode);
return verifyCode;
} /**
* 生成指定验证码图像文件
* @param w
* @param h
* @param outputFile
* @param code
* @throws IOException
*/
public static void outputImage(int w, int h, File outputFile, String code) throws IOException{
if(outputFile == null){
return;
}
File dir = outputFile.getParentFile();
if(!dir.exists()){
dir.mkdirs();
}
try{
outputFile.createNewFile();
FileOutputStream fos = new FileOutputStream(outputFile);
outputImage(w, h, fos, code);
fos.close();
} catch(IOException e){
throw e;
}
} /**
* 输出指定验证码图片流
* @param w
* @param h
* @param os
* @param code
* @throws IOException
*/
public static void outputImage(int w, int h, OutputStream os, String code) throws IOException{
int verifySize = code.length();
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Random rand = new Random();
Graphics2D g2 = image.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
Color[] colors = new Color[5];
Color[] colorSpaces = new Color[] { Color.WHITE, Color.CYAN,
Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
Color.PINK, Color.YELLOW };
float[] fractions = new float[colors.length];
for(int i = 0; i < colors.length; i++){
colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
fractions[i] = rand.nextFloat();
}
Arrays.sort(fractions); g2.setColor(Color.GRAY);// 设置边框色
g2.fillRect(0, 0, w, h); Color c = getRandColor(200, 250);
g2.setColor(c);// 设置背景色
g2.fillRect(0, 2, w, h-4); //绘制干扰线
Random random = new Random();
g2.setColor(getRandColor(160, 200));// 设置线条的颜色
for (int i = 0; i < 20; i++) {
int x = random.nextInt(w - 1);
int y = random.nextInt(h - 1);
int xl = random.nextInt(6) + 1;
int yl = random.nextInt(12) + 1;
g2.drawLine(x, y, x + xl + 40, y + yl + 20);
} // 添加噪点
float yawpRate = 0.05f;// 噪声率
int area = (int) (yawpRate * w * h);
for (int i = 0; i < area; i++) {
int x = random.nextInt(w);
int y = random.nextInt(h);
int rgb = getRandomIntColor();
image.setRGB(x, y, rgb);
} shear(g2, w, h, c);// 使图片扭曲 g2.setColor(getRandColor(100, 160));
int fontSize = h-4;
Font font = new Font("Algerian", Font.ITALIC, fontSize);
g2.setFont(font);
char[] chars = code.toCharArray();
for(int i = 0; i < verifySize; i++){
AffineTransform affine = new AffineTransform();
affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize/2, h/2);
g2.setTransform(affine);
g2.drawChars(chars, i, 1, ((w-10) / verifySize) * i + 5, h/2 + fontSize/2 - 10);
} g2.dispose();
ImageIO.write(image, "jpg", os);
} private static Color getRandColor(int fc, int bc) {
if (fc > 255)
fc = 255;
if (bc > 255)
bc = 255;
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
} private static int getRandomIntColor() {
int[] rgb = getRandomRgb();
int color = 0;
for (int c : rgb) {
color = color << 8;
color = color | c;
}
return color;
} private static int[] getRandomRgb() {
int[] rgb = new int[3];
for (int i = 0; i < 3; i++) {
rgb[i] = random.nextInt(255);
}
return rgb;
} private static void shear(Graphics g, int w1, int h1, Color color) {
shearX(g, w1, h1, color);
shearY(g, w1, h1, color);
} private static void shearX(Graphics g, int w1, int h1, Color color) { int period = random.nextInt(2); boolean borderGap = true;
int frames = 1;
int phase = random.nextInt(2); for (int i = 0; i < h1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(0, i, w1, 1, (int) d, 0);
if (borderGap) {
g.setColor(color);
g.drawLine((int) d, i, 0, i);
g.drawLine((int) d + w1, i, w1, i);
}
} } private static void shearY(Graphics g, int w1, int h1, Color color) { int period = random.nextInt(40) + 10; // 50; boolean borderGap = true;
int frames = 20;
int phase = 7;
for (int i = 0; i < w1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(i, 0, 1, h1, 0, (int) d);
if (borderGap) {
g.setColor(color);
g.drawLine(i, (int) d, i, 0);
g.drawLine(i, (int) d + h1, i, h1);
} } }
public static void main(String[] args) throws IOException
{
String verifyCode = generateVerifyCode(4);
System.out.println(verifyCode);
}
}

将生成的验证码放置到redis里,登录时候,从cookie取值,过滤器拦截验证(仅限PC端)

import com.google.code.kaptcha.impl.DefaultKaptcha;import io.swagger.annotations.ApiOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.TimeUnit; /**
*
* Description:用户相关接口
* @author huangweicheng
* @date 2019/10/22
*/
@RestController
@RequestMapping("/user")
public class UserController
{
private static final Logger log = LoggerFactory.getLogger(UserController.class); @Autowired
private RedisTemplate redisTemplate; @RequestMapping("/verifyCode.jpg")
@ApiOperation(value = "图片验证码")
public void verifyCode(HttpServletRequest request, HttpServletResponse response) throws IOException
{
/*禁止缓存*/
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 code = VerifyCodeUtils.generateVerifyCode(4);
/*验证码已key,value的形式缓存到redis 存放时间一分钟*/
log.info("验证码============>" + code);
String uuid = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(uuid,code,1,TimeUnit.MINUTES);
Cookie cookie = new Cookie("captcha",uuid);
/*key写入cookie,验证时获取*/
response.addCookie(cookie);
ServletOutputStream outputStream = response.getOutputStream();
//ImageIO.write(bufferedImage,"jpg",outputStream);
VerifyCodeUtils.outputImage(110,40,outputStream,code);
outputStream.flush();
outputStream.close();
}
}

尝试访问接口,生成的验证码是不是比组件生成的验证码好看多了。

验证码过滤器

验证码生成后,哪些地方需要用到验证码,配置对应的路径,设置过滤器进行过滤,过滤器继承OncePerRequestFilter,这样能够确保在一次请求只通过一Filter,而不需要重复执行,对应的路径没有正确的验证码抛出一个自定义的异常进行统一处理。

import com.alibaba.fastjson.JSONObject;import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set; /**
*
* Description: 图片验证码过滤器
* @author huangweicheng
* @date 2019/10/22
*/
@Component
public class ImageCodeFilter extends OncePerRequestFilter implements InitializingBean
{
/**
* 哪些地址需要图片验证码进行验证
*/
private Set<String> urls = new HashSet<>(); private AntPathMatcher antPathMatcher = new AntPathMatcher(); @Autowired
private RedisTemplate redisTemplate; @Override
public void afterPropertiesSet() throws ServletException
{
super.afterPropertiesSet();
urls.add("/hwc/user/login");
} @Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException
{
httpServletResponse.setContentType("application/json;charset=utf-8");
boolean action = false;
String t = httpServletRequest.getRequestURI();
for (String url : urls)
{
if (antPathMatcher.match(url,httpServletRequest.getRequestURI()))
{
action = true;
break;
}
}
if (action)
{
try {
/*图片验证码是否正确*/
checkImageCode(httpServletRequest);
}catch (ImageCodeException e){
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", ResultModel.ERROR);
jsonObject.put("msg",e.getMessage());
httpServletResponse.getWriter().write(jsonObject.toJSONString());
return;
}
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
/**
*
* Description:验证图片验证码是否正确
* @param httpServletRequest
* @author huangweicheng
* @date 2019/10/22
*/
private void checkImageCode(HttpServletRequest httpServletRequest)
{
/*从cookie取值*/
Cookie[] cookies = httpServletRequest.getCookies();
String uuid = "";
for (Cookie cookie : cookies)
{
String cookieName = cookie.getName();
if ("captcha".equals(cookieName))
{
uuid = cookie.getValue();
}
}
String redisImageCode = (String) redisTemplate.opsForValue().get(uuid);
/*获取图片验证码与redis验证*/
String imageCode = httpServletRequest.getParameter("imageCode");
/*redis的验证码不能为空*/
if (StringUtils.isEmpty(redisImageCode) || StringUtils.isEmpty(imageCode))
{
throw new ImageCodeException("验证码不能为空");
}
/*校验验证码*/
if (!imageCode.equalsIgnoreCase(redisImageCode))
{
throw new ImageCodeException("验证码错误");
}
redisTemplate.delete(redisImageCode);
}
}

自定义的验证码异常

import lombok.Data;

import java.io.Serializable;
/**
*
* Description:图片验证码相关异常
* @author huangweicheng
* @date 2019/10/22
*/
@Data
public class ImageCodeException extends RuntimeException implements Serializable
{
private static final long serialVersionUID = 4554L; private String code; public ImageCodeException()
{
} public ImageCodeException(String message)
{
super(message);
} public ImageCodeException(String code,String message)
{
super(message);
this.code = code;
} public ImageCodeException(String message,Throwable cause)
{
super(message,cause);
} public ImageCodeException(Throwable cause)
{
super(cause);
} public ImageCodeException(String message,Throwable cause,boolean enableSupperssion,boolean writablesStackTrace)
{
super(message,cause,enableSupperssion,writablesStackTrace);
} }

过滤器统一处理

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody; /**
*
* Description:全局变量捕获
* @author huangweicheng
* @date 2019/10/22
*/
@ControllerAdvice
public class GlobalExceptionHandler
{
@ResponseBody
@ExceptionHandler(Exception.class)
public ResponseEntity<ResultModel> exceptionHandler(Exception e)
{
e.printStackTrace();
ResultModel resultModel = new ResultModel(2,"系统出小差了,让网站管理员来处理吧 ಥ_ಥ");
return new ResponseEntity<>(resultModel, HttpStatus.OK);
} @ResponseBody
@ExceptionHandler(ImageCodeException.class)
public ResponseEntity<ResultModel> exceptionHandler(ImageCodeException e)
{
e.printStackTrace();
ResultModel resultModel = new ResultModel(2,e.getMessage());
return new ResponseEntity<>(resultModel,HttpStatus.OK);
}
}

说了这么多,只是我们token验证的开始

security

引入spring的security安全框架

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

最终的安全配置

import com.alibaba.fastjson.JSONObject;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.concurrent.TimeUnit; /**
*
* Description:安全配置
* @author huangweicheng
* @date 2019/10/21
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
/**
* 日志记录
*/
private static final Logger log = LoggerFactory.getLogger(Security.class); @Autowired
private RedisTemplate redisTemplate; @Autowired
protected SysUserDetailsServiceImpl sysUserDetailsService; @Autowired
private ImageCodeFilter imageCodeFilter; @Autowired
private JwtTokenUtil jwtTokenUtil;
/**
*
* Description:资源角色配置登录
* @param http
* @author huangweicheng
* @date 2019/10/21
*/
@Override
protected void configure(HttpSecurity http) throws Exception
{
/*图片验证码过滤器设置在密码验证之前*/
http.addFilterBefore(imageCodeFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/", "/*.html", "favicon.ico", "/**/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
.antMatchers("/user/**","/login").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/hwc/**").hasRole("USER")
.anyRequest().authenticated()
.and().formLogin().loginProcessingUrl("/user/login")
/*自定义登录成功处理,返回token值*/
.successHandler((HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication)->
{
log.info("用户为====>" + httpServletRequest.getParameter("username") + "登录成功");
httpServletResponse.setContentType("application/json;charset=utf-8");
/*获取用户权限信息*/
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String token = jwtTokenUtil.generateToken(userDetails);
/*存储redis并设置了过期时间*/
redisTemplate.boundValueOps(userDetails.getUsername() + "hwc").set(token,10, TimeUnit.MINUTES);
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", ResultModel.SUCCESS);
jsonObject.put("msg","登录成功");
/*认证信息写入header*/
httpServletResponse.setHeader("Authorization",token);
httpServletResponse.getWriter().write(jsonObject.toJSONString());
})
/*登录失败处理*/
.failureHandler((HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)->
{
log.info("用户为====>" + request.getParameter("username") + "登录失败");
String content = exception.getMessage();
//TODO 后期改进密码错误方式,统一处理
String temp = "Bad credentials";
if (temp.equals(exception.getMessage()))
{
content = "用户名或密码错误";
}
response.setContentType("application/json;charset=utf-8");
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", ResultModel.ERROR);
jsonObject.put("msg",content);
jsonObject.put("content",exception.getMessage());
response.getWriter().write(jsonObject.toJSONString());
})
/*无权限访问处理*/
.and().exceptionHandling().accessDeniedHandler((HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e)->
{
httpServletResponse.setContentType("application/json;charset=utf-8");
JSONObject jsonObject = new JSONObject();
jsonObject.put("code",HttpStatus.FORBIDDEN);
jsonObject.put("msg", "无权限访问");
jsonObject.put("content",e.getMessage());
httpServletResponse.getWriter().write(jsonObject.toJSONString());
})
/*匿名用户访问无权限资源时的异常*/
.and().exceptionHandling().authenticationEntryPoint((HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)->
{
response.setContentType("application/json;charset=utf-8");
JSONObject jsonObject = new JSONObject();
jsonObject.put("code",HttpStatus.FORBIDDEN);
jsonObject.put("msg","无访问权限");
response.getWriter().write(jsonObject.toJSONString());
})
.and().authorizeRequests()
/*基于token,所以不需要session*/
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
/*由于使用的是jwt,这里不需要csrf防护并且禁用缓存*/
.and().csrf().disable().headers().cacheControl();
/*token过滤*/
http.addFilterBefore(authenticationTokenFilterBean(),UsernamePasswordAuthenticationFilter.class);
} @Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception
{
authenticationManagerBuilder.userDetailsService(sysUserDetailsService).passwordEncoder(new PasswordEncoder()
{
/**
*
* Description:用户输入的密码加密
* @param charSequence
* @author huangweicheng
* @date 2019/10/21
*/
@Override
public String encode(CharSequence charSequence)
{
try {
return Common.md5(charSequence.toString());
}catch (NoSuchAlgorithmException e){
e.printStackTrace();
}
return null;
} /**
*
* Description: 与数据库的密码匹配
* @param charSequence 用户密码
* @param encodedPassWord 数据库密码
* @author huangweicheng
* @date 2019/10/21
*/
@Override
public boolean matches(CharSequence charSequence, String encodedPassWord)
{
try {
return encodedPassWord.equals(Common.md5(charSequence.toString()));
}catch (NoSuchAlgorithmException e){
e.printStackTrace();
}
return false;
}
});
}
  //token过滤器
@Bean
public JwtAuthenticationFilter authenticationTokenFilterBean()
{
return new JwtAuthenticationFilter();
}
}

注解很多都解释清楚,就不过多介绍了。因为security已经将实现登陆的功能封装完成,需要我们做的其实并不多,我们要做仅是查找用户,将查询用户的信息,包括密码,角色等等交给UserDtails,然后在配置里进行自定义验证(可以是md5或其他加密方式),持久层用的是jpa

用户类

import io.swagger.annotations.ApiModel;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List; /**
*
* Description:用户信息
* @author huangweicheng
* @date 2019/10/21
*/
@Entity
@Data
@ApiModel
@Table(name = "t_sys_user")
public class SysUserVo extends SysBaseVo implements UserDetails
{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private int id; @Column(name = "user_name")
private String userName; @Column(name = "password")
private String password; @Column(name = "error_num")
private int errorNum; @Column(name = "password_weak")
private int passwordWeak; @Column(name = "forbid")
private int forbid; @Column(name = "uuid")
private String uuid; /**
* CascadeType.REMOVE 级联删除,FetchType.LAZY懒加载,不会马上从数据库中加载
* name中间表名称
* @JoinColumn t_sys_user的user_id与中间表user_id的映射关系
* @inverseJoinColumns 中间表另一字段与对应表关联关系
*/
@ManyToMany(cascade = CascadeType.REMOVE,fetch = FetchType.EAGER)
@JoinTable(name = "t_sys_user_roles",joinColumns = @JoinColumn(name="user_id",referencedColumnName = "user_id"),inverseJoinColumns = @JoinColumn(name = "role_id",referencedColumnName = "role_id"))
private List<SysRoleVo> roles;
/**
*
* Description:权限信息
* @param
* @author huangweicheng
* @date 2019/10/21
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities()
{
List<GrantedAuthority> authorityList = new ArrayList<>();
List<SysRoleVo> roles = this.getRoles();
for (SysRoleVo role : roles)
{
authorityList.add(new SimpleGrantedAuthority(role.getRoleName()));
}
return authorityList;
} @Override
public String getUsername()
{
return this.userName;
} /**
*
* Description:账户是否过期
* @param
* @author huangweicheng
* @date 2019/10/21
*/
@Override
public boolean isAccountNonExpired()
{
return true;
} /**
*
* Description:账户是否被冻结
* @param
* @author huangweicheng
* @date 2019/10/21
*/
@Override
public boolean isAccountNonLocked()
{
if (forbid != 1)
{
return false;
}
return true;
} /**
*
* Description:账户密码是否过期,密码要求性高会使用到,比较每隔一段时间就要求用户重置密码
* @param
* @author huangweicheng
* @date 2019/10/21
*/
@Override
public boolean isCredentialsNonExpired()
{
return true;
} /**
*
* Description:账户是否可用
* @param
* @author huangweicheng
* @date 2019/10/21
*/
@Override
public boolean isEnabled()
{
if (bUse != 1)
{
return false;
}
return true;
}
}

角色类Role

import io.swagger.annotations.ApiModel;
import lombok.Data; import javax.persistence.*; @Entity
@Data
@ApiModel
@Table(name = "t_sys_role")
public class SysRoleVo extends SysBaseVo
{
@Id
@GeneratedValue
@Column(name = "role_id")
private int roleId; @Column(name = "role_name")
private String roleName;
}

因为我喜欢把相同的属性抽出来,所以定义了一个基类,也可以不这么干

import io.swagger.annotations.ApiModel;
import lombok.Data; import javax.persistence.*; @Entity
@Data
@ApiModel
@Table(name = "t_sys_role")
public class SysRoleVo extends SysBaseVo
{
@Id
@GeneratedValue
@Column(name = "role_id")
private int roleId; @Column(name = "role_name")
private String roleName;
}

接下来就简单多了,只需要在定义一个实现类去实现UserDetailService,基本的登录其实就完成了。

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; /**
*
* Description:账户详情信息
* @author huangweicheng
* @date 2019/10/21
*/
@Service
public class SysUserDetailsServiceImpl implements UserDetailsService
{
@Resource
private SysUserRepository sysUserRepository; @Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException
{
SysUserVo sysUser = sysUserRepository.findByUserName(userName);
if (sysUser == null)
{
throw new UsernameNotFoundException(userName);
}
return sysUser;
}
}

JWT

jwt的相关介绍就不多废话了,不了解可以查看阮大神的博客

JwtTokenUtil工具类(剽窃林老师的代码)

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component; import java.io.Serializable;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit; /**
*
* Description: token相关的工具类
* @author huangweicheng
* @date 2019/10/23
*/
@Component
public class JwtTokenUtil implements Serializable
{
private static final long serialVersionUID = -4324967L; private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
private static final String CLAIM_KEY_ROLES = "roles"; @Autowired
private RedisTemplate redisTemplate; @Value("${jwt.secret}")
private String secret; @Value("${jwt.expiration}")
private Long expiration; /**
*
* Description: 解析token,从token中获取信息
* @param token
* @author huangweicheng
* @date 2019/10/23
*/
private Claims getClaimsFromToken(String token)
{
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
e.printStackTrace();
claims = null;
}
return claims;
} /**
*
* Description:获取用户名
* @param token
* @author huangweicheng
* @date 2019/10/23
*/
public String getUserNameFromToken(String token)
{
String userName;
try {
final Claims claims = getClaimsFromToken(token);
userName = claims.getSubject();
}catch (Exception e){
userName = null;
}
return userName;
} /**
*
* Description:从token中获取
* @param token
* @author huangweicheng
* @date 2019/10/25
*/
public String getRolesFromToken(String token)
{
String roles;
try {
final Claims claims = getClaimsFromToken(token);
roles = (String) claims.get(CLAIM_KEY_ROLES);
}catch (Exception e){
roles = null;
}
return roles;
}
/**
*
* Description:获取token创建时间
* @param token
* @author huangweicheng
* @date 2019/10/23
*/
public Date getCreatedDateFromToken(String token)
{
Date created;
try {
final Claims claims = getClaimsFromToken(token);
created = new Date((Long) claims.get(CLAIM_KEY_CREATED));
}catch (Exception e){
created = null;
}
return created;
} /**
*
* Description: 获取token过期时间
* @param token
* @author huangweicheng
* @date 2019/10/23
*/
public Date getExpirationDateFromToken(String token)
{
Date expiration;
try {
final Claims claims = getClaimsFromToken(token);
expiration = claims.getExpiration();
}catch (Exception e){
expiration = null;
}
return expiration;
} /**
*
* Description:token生成过期时间
* @param
* @author huangweicheng
* @date 2019/10/23
*/
private Date generateExpirationDate()
{
return new Date(System.currentTimeMillis() + expiration * 1000);
} /**
*
* Description:token是否过期
* @param token
* @author huangweicheng
* @date 2019/10/23
*/
private Boolean isTokenExpired(String token)
{
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
} /**
*
* Description:token创建时间与密码最后修改时间比较,小于返回true,大于返回false
* @param created
* @param lastPasswordReset
* @author huangweicheng
* @date 2019/10/24
*/
private Boolean isCreatedBeforeLastPasswordReset(Date created,Date lastPasswordReset)
{
return (lastPasswordReset != null && created.before(lastPasswordReset));
}
/**
*
* Description: 创建token
* @param userDetails
* @author huangweicheng
* @date 2019/10/23
*/
public String generateToken(UserDetails userDetails)
{
String roles = "";
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
for (GrantedAuthority authority : authorities)
{
String temp = authority.getAuthority() + ",";
roles += temp;
}
roles = roles.substring(0,roles.length() - 1);
Map<String,Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME,userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED,new Date());
claims.put(CLAIM_KEY_ROLES,roles);
return generateToken(claims);
}
/**
*
* Description:使用Rs256签名
* @param claims
* @author huangweicheng
* @date 2019/10/23
*/
private String generateToken(Map<String,Object> claims)
{
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512,secret)
.compact();
} /**
*
* Description:是否刷新token
* @param token
* @param lastPasswordReset
* @author huangweicheng
* @date 2019/10/23
*/
public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
final Date created = getCreatedDateFromToken(token);
return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
&& !isTokenExpired(token);
} /**
*
* Description:刷新token
* @param token
* @author huangweicheng
* @date 2019/10/23
*/
public String refreshToken(String token)
{
String refreshToken;
try {
final Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED,new Date());
refreshToken = generateToken(claims);
}catch (Exception e){
refreshToken = null;
}
return refreshToken;
} /**
*
* Description:验证token
* @param token
* @param userDetails
* @author huangweicheng
* @date 2019/10/24
*/
public boolean validateToken(String token)
{
final String username = getUserNameFromToken(token);
if (redisTemplate.hasKey(username + "huangweicheng") && !isTokenExpired(token))
{
//如果验证成功,说明此用户进行了一次有效操作,延长token的过期时间
redisTemplate.boundValueOps(username + "subjectrace").expire(this.expiration,TimeUnit.MINUTES);
return true;
}
return false;
}

现在我们设置token过滤,请求接口没有token或者token已经过期,就会跳到登录页面

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List; /**
*
* Description:token的拦截器
* @author huangweicheng
* @date 2019/10/24
*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter
{
@Value("${jwt.header}")
private String tokenHeader; @Value("${jwt.tokenHead}")
private String tokenHead; @Autowired
private UserDetailsService userDetailsService; @Autowired
private JwtTokenUtil jwtTokenUtil; @Autowired
private RedisTemplate redisTemplate; @Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException
{
String token = httpServletRequest.getHeader(this.tokenHeader);
if (token != null && jwtTokenUtil.validateToken(token))
{
String role = jwtTokenUtil.getRolesFromToken(token);
String[] roles = role.split(",");
List<GrantedAuthority> authorityList = new ArrayList<>();
for (String r : roles)
{
authorityList.add(new SimpleGrantedAuthority(r));
}
String username = jwtTokenUtil.getUserNameFromToken(token);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,null,authorityList);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
/*权限设置*/
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}

现在验证的核心内容都已经完成,写几个接口测试下。

HomeController类

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody; @Controller
public class HomeController
{
@RequestMapping("/admin/test2")
@ResponseBody
public String admin2()
{
return "ROLE_ADMIN";
} }

HwcController类

@Controller
public class HwcController
{
@GetMapping("/hwc/test")
@ResponseBody
public String test()
{
return "ROLE_USER";
}
}

用postman测试一下,没token匿名访问

获取验证码后,将验证码写入cookie里,输入账号密码,登录


登录成功

token在放在header里

有token,没权限访问

有权限有token访问

补充

application.properties

#        ┏┓   ┏┓+ +
#   ┏┛┻━━━┛┻┓ + +
#   ┃       ┃  
#   ┃   ━   ┃ ++ + + +
#   ████━████ ┃+
#   ┃       ┃ +
#   ┃   ┻   ┃
#   ┃       ┃ + +
#   ┗━┓   ┏━┛
#     ┃   ┃           
#     ┃   ┃ + + + +
#     ┃   ┃       
#     ┃   ┃ +     神兽护体,代码 no bug  
#     ┃   ┃
#     ┃   ┃  +         
#     ┃    ┗━━━┓ + +
#     ┃        ┣┓
#     ┃        ┏┛
#     ┗┓┓┏━┳┓┏┛ + + + +
#      ┃┫┫ ┃┫┫
#      ┗┻┛ ┗┻┛+ + + + server.port=8080
server.servlet.context-path=/huangweicheng
server.servlet.session.cookie.http-only=true spring.http.encoding.force=true
##########################################
####jpa连接 ##
##########################################
spring.jpa.database = MYSQL
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.generate-ddl=true
#数据库连接
spring.datasource.url = jdbc:mysql://127.0.0.1:3306/hwc_db?characterEncoding=utf8&useSSL=true
spring.datasource.username = root
spring.datasource.password = root
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
#jwt 配置
jwt.header=Authorization
jwt.secret=huangweicheng
jwt.expiration=1000
#reids配置
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait=-1ms
# 连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
#日志配置
logging.path=D://log/
logging.file=huangweicheng.log
logging.level.root = INFO
#日志格式
logging.pattern.console=%d{yyyy/MM/dd-HH:mm:ss} [%thread] %-5level %logger- %msg%n
logging.pattern.file=%d{yyyy/MM/dd-HH:mm} [%thread] %-5level %logger- %msg%n

redis相关配置

/**
*
* Description:redis配置,EnableCaching开启缓存
* @author huangweicheng
* @date 2019/10/22
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
@Bean
@Override
public KeyGenerator keyGenerator()
{
return (o,method,objects)->
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(o.getClass().getName());
stringBuilder.append(method.getName());
for (Object obj : objects)
{
stringBuilder.append(obj.toString());
}
return stringBuilder.toString();
};
}
/**
*
* Description: redisTemplate序列化
* @param factory
* @author huangweicheng
* @date 2019/10/22
*/
@Bean
public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory factory)
{
RedisTemplate<Object,Object> redisTemplate = new RedisTemplate<Object, Object>();
redisTemplate.setConnectionFactory(factory);
FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
/*设置value值的序列化*/
redisTemplate.setValueSerializer(fastJsonRedisSerializer);
redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
/*设置key的序列化*/
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setDefaultSerializer(fastJsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}

数据库表
t_sys_user

t_sys_user_roles

t_sys_role

总结

jwt的token本应该是无状态的认证的,但没到过期时间这个token都是可用的,没法控制,在这期间如果被盗取,将会产生严重后果,所以引入redis控制状态。而且这还是不够严谨,应该进一步引入https的认证。增加信息的安全性,这只是一个demo,如果有需要,请留言,将会整理到码云或github上提供下载。

springboot security+redis+jwt+验证码 登录验证的更多相关文章

  1. SpringBoot实现基于token的登录验证

    一.SpringBoot实现基于token的登录验证 基于token的登录验证实现原理:客户端通过用户名和密码调用登录接口,当验证数据库中存在该用户后,将用户的信息按照token的生成规则,生成一个字 ...

  2. Spring Security 一键接入验证码登录和小程序登录

    最近实现了一个多端登录的Spring Security组件,用起来非常丝滑,开箱即用,可插拔,而且灵活性非常强.我觉得能满足大部分场景的需要.目前完成了手机号验证码和微信小程序两种自定义登录,加上默认 ...

  3. Spring Security 实现手机验证码登录

    思路:参考用户名密码登录过滤器链,重写认证和授权 示例如下(该篇示例以精简为主,演示主要实现功能,全面完整版会在以后的博文中发出): 由于涉及内容较多,建议先复制到本地工程中,然后在细细研究. 1. ...

  4. php 连接redis,并登录验证

    环境: centos7 上安装了redis, 同时安装了php的redis扩展 yum install redis yum install php-pecl-redis redis服务端设置了登录密码 ...

  5. spring boot:spring security整合jwt实现登录和权限验证(spring boot 2.3.3)

    一,为什么使用jwt? 1,什么是jwt? Json Web Token, 它是JSON风格的轻量级的授权和身份认证规范, 可以实现无状态.分布式的Web应用授权 2,jwt的官网: https:// ...

  6. Spring Security和JWT实现登录授权认证

     目标 1.Token鉴权 2.Restful API 3.Spring Security+JWT 开始 自行新建Spring Boot工程 引入相关依赖 <dependency> < ...

  7. Springboot+SpringSecurity实现图片验证码登录问题

    这个问题,网上找了好多,结果代码都不全,找了好多,要不是就自动注入的类注入不了,编译报错,要不异常捕获不了浪费好多时间,就觉得,框架不熟就不能随便用,全是坑,气死我了,最后改了两天.终于弄好啦; 问题 ...

  8. Spring Security 入门(3-11)Spring Security 的使用-自定义登录验证和回调地址

    配置文件 security-ns.xml <?xml version="1.0" encoding="UTF-8"?> <beans xmln ...

  9. SpringBoot整合redis把用户登录信息存入redis

    首先引入redis的jai包 <dependency> <groupId>org.springframework.boot</groupId> <artifa ...

随机推荐

  1. maven的pom.xml详解

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/20 ...

  2. Java面试-interrupt

    我们都知道,Java中停止一个线程不能用stop,因为stop会瞬间强行停止一个线程,且该线程持有的锁并不能释放.大家多习惯于用interrupt,那么使用它又有什么需要注意的呢? interrupt ...

  3. Collections.unmodifiableMap,Collections.unmodifiableList,Collections.unmodifiableSet作用及源码解析

    在文章:Mybatis源码解析,一步一步从浅入深(五):mapper节点的解析中mybatis的源码中用到了Collections.unmodifiableList方法,其实还有unmodifiabl ...

  4. 2018年蓝桥杯java b组第七题

    标题:螺旋折线 如图p1.pgn所示的螺旋折线经过平面上所有整点恰好一次. 对于整点(X, Y),我们定义它到原点的距离dis(X, Y)是从原点到(X, Y)的螺旋折线段的长度. 例如dis(0, ...

  5. 阿里云服务器ecs配置之安装jdk

    一.安装环境 操作系统:Centos 7.4 JDK版本:1.8 工具:Xshell5.Xftp5 二.安装步骤 第一步:下载安装包 (官网)链接: 下载适合自己系统的jdk版本,如图:我下载的是64 ...

  6. php数字函数

    is_numeric() 检查变量是否包含一个合法数字 round()  取整数,四舍五入 round(数字, 小数位) ceil()  向上取整 floor() 向下取整 range()  生成范围 ...

  7. docker升级后启动报错400 Client Error: Bad Request ("Unknown runtime specified docker-runc")

    宝塔面板docker升级后启动容器时报错400 Client Error: Bad Request ("Unknown runtime specified docker-runc" ...

  8. 用CSS绘制实体三角形并说明原理

    ;;margin:0 auto;border:6px solid transparent;border-top: 6px solid red;} 1.采用的是均分原理 盒子都是一个矩形或正方形,从形状 ...

  9. B-概率论-常见的概率分布模型

    目录 常见的概率分布模型 一.离散概率分布函数 二.连续概率分布函数 三.联合分布函数 四.多项分布(Multinomial Distribution) 4.1 多项分布简介 4.2 多项分布公式解析 ...

  10. java并发之CAS详解

    前言 在高并发的应用当中,最关键的问题就是对共享变量的安全访问,通常我们都是通过加锁的方式,比如说synchronized.Lock来保证原子性,或者在某些应用当中,用voliate来保证变量的可见性 ...