Springsecurity集成jwt+redis实现验证码登录注册

前言

先pizha一下(长文预警)

github地址:github地址 下载或克隆点这里!!

正文

本demo是spring security集成jwt进行登录的校验,同时引入redis来存放验证码进行校验工作,利用redis的设置过期时间的特性。

引入依赖

这里只贴出重要的几个,详情请看github.

 <!-- jwt依赖 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.mobile</groupId>
            <artifactId>spring-mobile-device</artifactId>
            <version>1.1.5.RELEASE</version>
        </dependency>
        <!-- redis依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- security依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- Swagger依赖-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.8.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.8.0</version>
        </dependency>

重要的依赖差不多就这几个了。

自定义一个UserDetails接口的user类

@Data
public class JwtUser implements UserDetails {

    private String stuId;

    private String password;

    public JwtUser(String stuId, String password) {
        this.stuId = stuId;
        this.password = password;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return stuId;
    }

    @Override
    public String getUsername() {
        return null;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

自定义一个UserDetailService的实现类,实现自定义用户的校验

其中对于数据库的访问每个人都不同,所以并没有给出代码,需要有一个通过用户输入的用户名来查询用户信息的方法getUserByUserId(userId).

Token工具类

@Service
    @Slf4j
    public class JwtTokenUtil {
        public static final long seriaVersionUID = -3301605591108950415L;

        static final String CLAIM_KEY_USERNAME = "sub";
        static final String CLAIM_KEY_AUDIENCE = "audience";
        static final String CLAIM_KEY_CREATED = "created";

        private static final String AUDIENCE_UNKNOWN = "unknown";
        private static final String AUDIENCE_WEB = "web";
        private static final String AUDIENCE_MOBILE = "mobile";
        private static final String AUDIENCE_TABLET = "tablet";

        //当前的签名的秘钥
        private String secret = "cdd";
        //token的有效时间 约25min
        private Long expiration = 1296000L;

        public String getUsernameFromToken(String token) {
            String username;
            try {
                final Claims claims = getClaimsFromToken(token);
                username = claims.getSubject();
            } catch (Exception e) {
                username = null;
            }
            return username;
        }

        public Date getCreatDateFromToken(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;
        }

        //得到token的有效期
        private Date getExpirationDateFromToken(String token) {
            Date expiration;
            try {
                final Claims claims = getClaimsFromToken(token);
                expiration = claims.getExpiration();
            } catch (Exception e) {
                expiration = null;
            }
            return expiration;
        }

        public String getAudienceFromToken(String token) {
            String audience;
            try {
                final Claims claims = getClaimsFromToken(token);
                audience = (String) claims.get(CLAIM_KEY_AUDIENCE);
            } catch (Exception e) {
                audience = null;
            }
            return audience;
        }

        private Claims getClaimsFromToken(String token) {
            Claims claims;
            try {
                claims = Jwts.parser()
                        .setSigningKey(secret)
                        .parseClaimsJws(token)
                        .getBody();
            } catch (Exception e) {
                claims = null;
            }
            return claims;
        }

        //设置过期时间
        private Date generateExpeirationDate() {
            return new Date(System.currentTimeMillis() + expiration * 1000);
        }

        private Boolean isTokenExpired(String token) {
            final Date expiration = getExpirationDateFromToken(token);
            return expiration.before(new Date());
        }

        private Boolean isCreatedAfterTenMinutes(Date created) {
            int minutes = (int) ((System.currentTimeMillis() - created.getTime()) / (1000 * 60));
            if (minutes >= 10) {
                return true;
            }
            return false;
        }

        private String generateAudience(Device device) {
            String audience = AUDIENCE_UNKNOWN;
            if (device.isNormal()) {
                audience = AUDIENCE_WEB;
            } else if (device.isTablet()) {
                audience = AUDIENCE_TABLET;
            } else if (device.isMobile()) {
                audience = AUDIENCE_MOBILE;
            }
            return audience;
        }

        private Boolean ignoreTokenExpiration(String token) {
            String audience = getAudienceFromToken(token);
            return (AUDIENCE_TABLET.equals(audience) || AUDIENCE_MOBILE.equals(audience));
        }


        String generateToken(Map<String, Object> claims) {
            return Jwts.builder()
                    .setClaims(claims)
                    .setExpiration(generateExpeirationDate())
                    .signWith(SignatureAlgorithm.HS512, secret)
                    .compact();
        }

        public String generateToken(UserDetails userDetails) {
            Map<String, Object> claims = new HashMap<>();
            claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
            claims.put(CLAIM_KEY_CREATED, new Date());
            return generateToken(claims);
        }

        //判断是否在10分钟后并在有效期内
        public Boolean canTokenBeRefreshed(String token) {
            final Date created = getCreatDateFromToken(token);
            return token != null && created != null && isCreatedAfterTenMinutes(created)
                    && (!isTokenExpired(token)) || ignoreTokenExpiration(token);
        }

        public String refreshToken(String token) {
            String refreshedToken;
            try {
                final Claims claims = getClaimsFromToken(token);
                claims.put(CLAIM_KEY_CREATED, new Date());
                refreshedToken = generateToken(claims);
            } catch (Exception e) {
                refreshedToken = null;
            }
            log.info("获取要刷新的token: {}", refreshedToken);
            return refreshedToken;
        }

        public Boolean validateToken(String token, UserDetails userDetails) {
            JwtUser user = (JwtUser) userDetails;
            final String username = getUsernameFromToken(token);
            final Date created = getCreatDateFromToken(token);
            return (username.equals(user.getUsername())) && !isTokenExpired(token);
        }
    }

具体的安全校验的我就不一一贴出来了 需要的请直接看git上的源码

进入正题

实现过程

  1. 新建一个工具类 RandomUtil 用来生成验证码
  2. 引入 RedisUtil 方便操作redis存储(具体怎么写请参照我之前的博客)
  3. 编写service层和controller层 其他实现请参看我的git源码

创建RandomUtil

生成五位数验证码

public class RandomUtil {
    public static int returnCode(){
        Random rand = new Random();
        return rand.nextInt(89999)+10000;
    }
}

引入RedisUtil

请翻阅我之前的博客 
链接:https://blog.csdn.net/qq_43561507/article/details/101025270
个人认为封装较为完整

编写Controller (共三个)

  1. 获取验证码
    生成验证码后立即存入redis中(我设置的过期时间为120s)
    注意! code这个接口需要在WebSecurityConfig中打开 可直接访问 不然会被拦截
@RestController
@Slf4j
@RequestMapping("/code")
@Api(tags = "验证码生成接口")
@CrossOrigin
public class SecurityCodeController {
    @Autowired
    private RedisUtil redisUtil;
    @PostMapping("/getCode")
    @ApiOperation("获取验证码")
    @RoleContro(role = RoleEnum.ADMIN)
    public Object getCode(@RequestParam String username){
        String optCode = String.valueOf(RandomUtil.returnCode());
        redisUtil.set(username,optCode,120);
        //这里由于没有用短信发送 为了获取验证码是多少 我们打印到控制台
        log.info(optCode);
        return ResultVOUtil.success(optCode);
    }
}

  1. 注册
@Slf4j
@RequestMapping("/admin")
@RestController
@Api(tags = "注册接口")
@CrossOrigin
public class AdminController {

    @Autowired
    private UserService userService;

    @PostMapping("/addUser")
    @ApiOperation("添加用户")
    @RoleContro(role = RoleEnum.ADMIN)
    public Object addUser(UserDTO userDTO) {

        return userService.addUser(userDTO);
    }
}**
  1. 登录
@Slf4j
@RestController
@RequestMapping("/anon")
@Api(tags = "登录接口")
@CrossOrigin
public class AnonController {

    @Autowired
    private UserService userService;

    @ApiOperation("登录")
    @PostMapping("/login")
    public ResultVO login(@Valid LoginForm loginForm, HttpServletResponse response) {
        return userService.login(loginForm, response);
    }
}

验证码的校验放在Service层中

可能是redis读出乱码的原因 我将他们全部转化为了int类型方便比较 具体原因我还需要检查下 先就这样吧

package com.jwt.redis.service.serviceImpl;

import com.jwt.redis.DTO.UserDTO;
import com.jwt.redis.dao.UserMapper;
import com.jwt.redis.entity.User;
import com.jwt.redis.enums.ResultEnum;
import com.jwt.redis.form.LoginForm;
import com.jwt.redis.security.JwtProperties;
import com.jwt.redis.security.JwtUserDetailServiceImpl;
import com.jwt.redis.service.UserService;
import com.jwt.redis.util.*;
import com.jwt.redis.vo.ResultVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * @author: zty
 * @date 2019/9/5 下午4:49
 * @description:
 */
@Service
@Slf4j
public class UserServiceImpl implements UserService {

//    private final String DEFAULT_PASSWORD = "123456";

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private JwtUserDetailServiceImpl jwtUserDetailService;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private JwtProperties jwtProperties;

    @Override
    public User getCurrentUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String userName = authentication.getName();
        String key = "anonymousUser";
        if (!userName.equals(key)) {
            return getUserByUsername(userName);
        }
        return null;
    }

    @Override
    public User getUserByUsername(String userName) {
        return userMapper.selectUserByUsername(userName);
    }

    /**
     * Security自带的
     *
     * @param loginForm
     * @param response
     * @return
     */
    @Override
    public ResultVO login(LoginForm loginForm, HttpServletResponse response) {
        int code = Integer.valueOf(loginForm.getSecurityCode());

        String code2 = (String) redisUtil.get(loginForm.getUsername());
        int redisCode = Integer.valueOf(code2);
        if(code==redisCode) {
            User user = userMapper.selectUserByUsername(loginForm.getUsername());
            if (user == null) {
                return ResultVOUtil.error(ResultEnum.USER_NOT_EXIST);
            }
            UserDetails userDetails = jwtUserDetailService.loadUserByUsername(loginForm.getUsername());
            if (!(new BCryptPasswordEncoder().matches(loginForm.getPassword(), userDetails.getPassword()))) {
                return ResultVOUtil.error(ResultEnum.PASSWORD_ERROR);
            }
            Authentication token = new UsernamePasswordAuthenticationToken(loginForm.getUsername(), loginForm.getPassword(), userDetails.getAuthorities());
            Authentication authentication = authenticationManager.authenticate(token);

            SecurityContextHolder.getContext().setAuthentication(authentication);

            final String realToken = jwtTokenUtil.generateToken(userDetails);
            response.addHeader(jwtProperties.getTokenName(), realToken);

            Map map = new HashMap();
            map.put("role", user.getRole());
            map.put("token", realToken);
            return ResultVOUtil.success(map);
        }
        return ResultVOUtil.error(ResultEnum.CODE_ERROR);
    }


    @Override
    public ResultVO addUser(UserDTO userDTO) {
        int code = Integer.valueOf(userDTO.getSecurityCode());

        String code2 = (String) redisUtil.get(userDTO.getUsername());
        int redisCode = Integer.valueOf(code2);
        if (code == redisCode) {
            User user = new User();
            BeanUtils.copyProperties(userDTO, user);
            user.setPassword(passwordEncoder.encode(userDTO.getPassword()));
            log.info("用户信息" + user);
            int result = userMapper.insert(user);
            if (result != 1) {
                return ResultVOUtil.error(ResultEnum.SQL_ERROR);
            }
            return ResultVOUtil.success();
        }
        return ResultVOUtil.error(ResultEnum.CODE_ERROR);
    }
}

我用的if来进行校验

大概就这么多了 看看效果吧

如果需要完整的请翻阅我的github

swagger打开有三个接口

先点击获取验证码 (类似于你注册时输入手机号 点击获取验证码 这里我没有写在一起 前端给我整合在一起就行啦)
在这里插入图片描述
由于我没有接入云短信的API 我就将随机生成的验证码返回以及打印到控制台来查看
72229

这个验证码有效期120秒
所以我们迅速去注册
在这里插入图片描述
在这里插入图片描述
注册成功!数据库中有了该条信息(密码已经加密)
在这里插入图片描述
登录过程也是一样的 获取验证码 登录

有什么问题欢迎留言

发布了22 篇原创文章 · 获赞 28 · 访问量 2660

猜你喜欢

转载自blog.csdn.net/qq_43561507/article/details/102229271