到目前,我学会了两种方法,在这里分享给大家。
第一种
在使用Spring Security框架过程中,经常会有这样的需求,即在登录验证时,附带增加额外的数据,如验证码、用户类型等。下面将介绍如何实现。
第一步:实现自定义的WebAuthenticationDetails
该类提供了获取用户登录时携带的额外信息的功能,默认实现WebAuthenticationDetails提供了remoteAddress与sessionId信息。开发者可以通过Authentication的getDetails()获取WebAuthenticationDetails。我们编写自定义类CustomWebAuthenticationDetails继承自WebAuthenticationDetails,添加我们关心的数据,以字段“demo"来表示。
public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {
private static final long serialVersionUID = 6975601077710753878L;
private final String demo;
public CustomWebAuthenticationDetails(HttpServletRequest request) {
super(request);
demo = request.getParameter("demo");
}
public String getDemo() {
return demo;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(super.toString()).append("; Demo: ").append(this.getDemo());
return sb.toString();
}
}
注:在登录页面,可将token字段放在form表单中,也可以直接加在url的参数中,进而把额外数据发送给后台。
第二步:实现自定义的AuthenticationDetailsSource
该接口用于在Spring Security登录过程中对用户的登录信息的详细信息进行填充,默认实现是WebAuthenticationDetailsSource,生成上面的默认实现WebAuthenticationDetails。我们编写类实现AuthenticationDetailsSource,用于生成上面自定义的CustomWebAuthenticationDetails。
@Component
public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
return new CustomWebAuthenticationDetails(context);
}
}
第三步:配置使用自定义的AuthenticationDetailsSource
只要看这一句.formLogin().authenticationDetailsSource(authenticationDetailsSource)
别忘了注入:
@Autowired
private CustomAuthenticationDetailsSource authenticationDetailsSource;
第四步:实现自定义的AuthenticationProvider
AuthenticationProvider提供登录验证处理逻辑,我们实现该接口编写自己的验证逻辑。
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private HttpServletRequest request;
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails(); // 如上面的介绍,这里通过authentication.getDetails()获取详细信息
// System.out.println(details); details.getRemoteAddress(); details.getSessionId(); details.getDemo();
// 下面是验证逻辑,验证通过则返回UsernamePasswordAuthenticationToken,
// 否则,可直接抛出错误(AuthenticationException的子类,在登录验证不通过重定向至登录页时可通过session.SPRING_SECURITY_LAST_EXCEPTION.message获取具体错误提示信息)
if(!request.getSession().getAttribute("text").toString().equalsIgnoreCase(details.getDemo()))//用户输入的和验证码不一致
{
System.out.println(request.getSession().getAttribute("text").toString()+" "+details.getDemo());
throw new BadCredentialsException("验证码不正确");
}
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
第五步:配置使用自定义的AuthenticationProvider
@Override
public void configure(AuthenticationManagerBuilder auth)throws Exception{
auth.authenticationProvider(myAuthenticationProvider);
}
第二种
第一步:写个产生验证码的类:
public class VerifyCode {
private int w = 70;
private int h = 35;
private Random r = new Random();
// {"宋体", "华文楷体", "黑体", "华文新魏", "华文隶书", "微软雅黑", "楷体_GB2312"}
private String[] fontNames = {"宋体", "华文楷体", "黑体", "微软雅黑", "楷体_GB2312"};
// 可选字符
private String codes = "0123456789abcdefghjkmnopqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ";
// 背景色
private Color bgColor = new Color(255, 255, 255);
// 验证码上的文本
private String text ;
public VerifyCode() {
}
public VerifyCode(LocalDateTime localDateTime) {
this.localDateTime = localDateTime;
}
public VerifyCode(int second) {
// 多少秒后
this.localDateTime = LocalDateTime.now().plusSeconds(second);
}
//过期时间
private LocalDateTime localDateTime;
// 生成随机的颜色
private Color randomColor () {
int red = r.nextInt(150);
int green = r.nextInt(150);
int blue = r.nextInt(150);
return new Color(red, green, blue);
}
// 生成随机的字体
private Font randomFont () {
int index = r.nextInt(fontNames.length);
String fontName = fontNames[index];//生成随机的字体名称
int style = r.nextInt(4);//生成随机的样式, 0(无样式), 1(粗体), 2(斜体), 3(粗体+斜体)
int size = r.nextInt(5) + 24; //生成随机字号, 24 ~ 28
return new Font(fontName, style, size);
}
// 画干扰线
private void drawLine (BufferedImage image) {
int num = 3;//一共画3条
Graphics2D g2 = (Graphics2D)image.getGraphics();
for(int i = 0; i < num; i++) {//生成两个点的坐标,即4个值
int x1 = r.nextInt(w);
int y1 = r.nextInt(h);
int x2 = r.nextInt(w);
int y2 = r.nextInt(h);
g2.setStroke(new BasicStroke(1.5F));
g2.setColor(Color.BLUE); //干扰线是蓝色
g2.drawLine(x1, y1, x2, y2);//画线
}
}
// 随机生成一个字符
private char randomChar () {
int index = r.nextInt(codes.length());
return codes.charAt(index);
}
// 创建BufferedImage
private BufferedImage createImage () {
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = (Graphics2D)image.getGraphics();
g2.setColor(this.bgColor);
g2.fillRect(0, 0, w, h);
return image;
}
// 调用这个方法得到验证码
public BufferedImage getImage () {
BufferedImage image = createImage();//创建图片缓冲区
Graphics2D g2 = (Graphics2D)image.getGraphics();//得到绘制环境
StringBuilder sb = new StringBuilder();//用来装载生成的验证码文本
// 向图片中画4个字符
for(int i = 0; i < 4; i++) {//循环四次,每次生成一个字符
String s = randomChar() + "";//随机生成一个字母
sb.append(s); //把字母添加到sb中
float x = i * 1.0F * w / 4; //设置当前字符的x轴坐标
g2.setFont(randomFont()); //设置随机字体
g2.setColor(randomColor()); //设置随机颜色
g2.drawString(s, x, h-5); //画图
}
this.text = sb.toString(); //把生成的字符串赋给了this.text
drawLine(image); //添加干扰线
return image;
}
// 返回验证码图片上的文本
public String getText () {
return text;
}
// 保存图片到指定的输出流
public static void output (BufferedImage image, OutputStream out)
throws IOException {
ImageIO.write(image, "JPEG", out);
}
public LocalDateTime getLocalDateTime() {
return localDateTime;
}
public void setLocalDateTime(LocalDateTime localDateTime) {
this.localDateTime = localDateTime;
}
public boolean isExpired(){
return LocalDateTime.now().isAfter(localDateTime);
}
}
第二步:写个代码抛出异常的封装类
public class ValidateCodeException extends AuthenticationException {
private static final long serialVersionUID = 1L;
public ValidateCodeException(String msg) {
super(msg);
}
}
第三步:配置验证码过滤器
在spring中,filter都默认继承OncePerRequestFilter,但为什么要这样呢?
OncePerRequestFilter顾名思义,他能够确保在一次请求只通过一次filter,而不需要重复执行
常识上都认为,一次请求本来就只过一次,为什么还要由此特别限定呢,实际上此方式是为了兼容不同的web container,特意而为之(jsr168),
也就是说并不是所有的container都像我们期望的只过滤一次,servlet版本不同,表现也不同
因此,为了兼容各种不同的运行环境和版本,默认filter继承OncePerRequestFilter是一个比较稳妥的选择
public class ValidateCodeFilter extends OncePerRequestFilter {
private MyAuthenticationFailHander myAuthenticationFailHander;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (StringUtils.equals("/login/form", request.getRequestURI())
&&StringUtils.equalsIgnoreCase(request.getMethod(), "post")) {
try {
System.out.println("验证码");
validate(new ServletWebRequest(request));
}
catch (ValidateCodeException e) {
System.out.println("验证码");
myAuthenticationFailHander.onAuthenticationFailure(request, response, e);
// 不继续执行
return;
}
}
// 继续执行下一步
filterChain.doFilter(request, response);
}
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
System.out.println("验证码");
// 从Session中获取imageCode对象
VerifyCode verifyCode = (VerifyCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
System.out.println("从session获取在imagecode对象"+verifyCode+" "+codeInRequest);
if (StringUtils.isBlank(codeInRequest)) {
System.out.println("纳尼???为空");
throw new ValidateCodeException("验证码为空或者不存在");
}
if(verifyCode == null){
System.out.println("纳尼???不存在");
throw new ValidateCodeException("验证码不存在,请刷新验证码");
}
if (verifyCode.isExpired()) {
//从session移除过期的验证码
System.out.println("纳尼???过期");
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
throw new ValidateCodeException("验证码过期");
}
if (!StringUtils.equalsIgnoreCase(verifyCode.getText(), codeInRequest)) {
System.out.println("纳尼???不匹配");
throw new ValidateCodeException("验证码不匹配");
}
// session 中移除key
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
}
public MyAuthenticationFailHander getMyAuthenticationFailHander() {
return myAuthenticationFailHander;
}
public void setMyAuthenticationFailHander(MyAuthenticationFailHander myAuthenticationFailHander) {
this.myAuthenticationFailHander = myAuthenticationFailHander;
}
}
上面的StringUtils.equals("/login/form", request.getRequestURI()中,引号里的是对应安全控制中心的loginProcessingUrl
第四步:生成验证码Control
@RestController
public class ValidateCodeController {
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
@GetMapping("/verifycode/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 1.根据随机数生成图片
VerifyCode vc = new VerifyCode(60);//设置60秒过期
/*ImageCode imageCode = createImageCode(request);*/
// 2.将图片存入session中
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, vc);
// 3.将生成的图片写入到接口响应中
ImageIO.write(vc.getImage(), "JPEG", response.getOutputStream());
}
}
第五步:SecurityConfig里进行过滤器配置
//验证码过滤器
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
//验证码过滤器中使用自己的错误处理
validateCodeFilter.setMyAuthenticationFailHander(myAuthenticationFailHander);
第六步:前台
<img src="/verifycode/image" style="margin-left: 30px;">
<input type="button" value="看不清? 换一张." style="margin-left: 30px;" id="btn">
<script type="text/javascript">
document.getElementById("btn").onclick = function () {
// 获取img元素
// 为了让浏览器发送请求到servlet, 所以一定要改变src
document.getElementsByTagName("img")[0].src =
"/verifycode/image?time=" + new Date().getTime();
};
</script>
效果展示:
好,到这里就完成了一个简陋的二维码的实现。