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

  • 相关阅读:
    用.net开发wap
    MVC3 中使用 Ajax.ActionLink Ajax.BeginForm
    收藏一下这个微软MVP的老外博客
    第三篇:Django的路由系统
    第二篇:Django自定义登录功能
    第一篇:Django简介
    json和pickle序列化模块
    oracle 11gr2 rac修改VIP
    修改监听端口号
    删除磁盘组
  • 原文地址:https://www.cnblogs.com/dslx/p/11751312.html
Copyright © 2020-2023  润新知