springboot security+redis+jwt+验证码 登录验证

概述

  基于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上提供下载。

原文地址:https://www.cnblogs.com/dslx/p/11751312.html