springboot + spring security Token认证 知识整理

流程图

 

一.理论知识

1.说明

Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。认证成功后将生成一个Token返回前端,供后续操作的验证。

2.授权

用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。

一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

Spring和Token整合,其实是对Spring Security 添加登陆和验证filter,不再以session作为登陆验证的标准,而是每次从请求中取token进行校验,如果token是正确的,解析出用户信息并交给Spring Security进行下一步操作。

二.具体流程

1.登录

请求为:/auth/login,RequsetBody 为用户名和密码。请求到达之后,会先到达TokenFilter,如果是初次登陆未携带token,请求将到达具体的登陆处理controller。如果不是初次登陆,TokenFilter将刷新失效时间,然后将新的LoginUser放入security上下文。

登陆时,service先通过用户名查数据库得到user对象,然后比对请求中的密码和查出来的对象中的密码。

密码校验

因为库中的密码是加过密的,因此需要不能直接比对,需要将request中的密码编码后和库中的密码进行对比,如果flag为true,则密码校验通过。

boolean flag = bCryptPasswordEncoder.matches(password, userEntity.getPassword());

生成Token 

验证通过后,service将查询好的user放入security上下文,然后生成Token:

UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(userEntity.getUsername(), password);
Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication); 

 接下来进行token的生成及缓存,先生成token并set到loginUser中:

loginUser.setToken(UUID.randomUUID().toString());

存放redis

然后将token和用户缓存到redis,这里缓存的token是uuid值:f53ca20a-d4bb-45ae-9158-feaf16b6ea78 

loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expire * 1000);
redisTemplate.boundValueOps(getTokenKey(loginUser.getToken())).set(loginUser, expire, TimeUnit.SECONDS);

创建JWT令牌

然后通过loginUser创建JWT令牌,做了加密处理:eyJhbGciOiJIUzI1NiJ9.eyJMT0dJTl9VU0VSX0tFWSI6ImY1M2NhMjBhLWQ0YmItNDVhZS05MTU4LWZlYWYxNmI2ZWE3OCJ9.7jk49ZC1p2pGX4Xigey02a9YFXn5WeM2I0PDlug2yMo 

Map<String, Object> claims = new HashMap<>();
claims.put(LOGIN_USER_KEY, loginUser.getToken());
String jwtToken = Jwts.builder().setClaims(claims)
		.signWith(SignatureAlgorithm.HS256, getKeyInstance())
		.compact();

然后把令牌返回前端: 

return new Token(jwtToken, loginUser.getLoginTime());

代码:

无token:

 有token

 2.其他请求

请求为 /auth/current,请求头为 token

首先经过 TokenFilter 过滤器 public class TokenFilter extends OncePerRequestFilter{},执行其 doFilterInternal 方法

然后获得

jwtToken:eyJhbGciOiJIUzI1NiJ9.eyJMT0dJTl9VU0VSX0tFWSI6ImQ0MzFlOGMzLTA2ZTctNDE5OC1hZThjLWYwOTkwYmQ3OTJmMiJ9.onJvDZDHoDek7WFxrKHcxIwl-RYStOq1uQKrJEmwStE

jwtToken = request.getHeader(TOKEN_KEY);

解析

然后将jwtToken解析为生成的uuid的token:LOGIN_USER_KEY -> d431e8c3-06e7-4198-ae8c-f0990bd792f2

Map<String, Object> jwtClaims = Jwts.parser()
	.setSigningKey(getKeyInstance())
	.parseClaimsJws(jwtToken).getBody();
token = MapUtils.getString(jwtClaims, LOGIN_USER_KEY);

redis中查询 

然后使用token去redis中查询,如果正确则得到 LoginUser 对象:

loginUser = redisTemplate.boundValueOps(getTokenKey(token)).get();

如果 loginUser 不为 null,那么将过期时间与当前时间对比,临近过期10分钟内的话,自动刷新redis缓存,也就是重新保存 

redisTemplate.boundValueOps(getTokenKey(loginUser.getToken())).set(loginUser, expire, TimeUnit.SECONDS);

然后重新生成 authentication 对象,通过 SecurityContextHolder 存入 SecurityContext

UsernamePasswordAuthenticationToken authentication = 
	new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);

这样filter通过后才到达相应的controller,到达后执行方法得到需要的loginUser 

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return (LoginUser) authentication.getPrincipal();
// 代码在 UserUtils 中

 代码

首先定义一个过滤器用于从请求中获取token

package com.wensi.auth.adapter;

@Component
public class TokenFilter extends OncePerRequestFilter {

    public static final String TOKEN_KEY = "token";
    private static final Long MINUTES_10 = 10 * 60 * 1000L;

    @Autowired
    private TokenService tokenService;

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        String jwtToken = getToken(request);
        if (StringUtils.isNotBlank(jwtToken)) {
            LoginUser loginUser = tokenService.getLoginUser(jwtToken);
            if (loginUser != null) {
                loginUser = checkLoginTime(loginUser);
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        chain.doFilter(request, response);
    }

    //校验时间, 过期时间与当前时间对比,临近过期10分钟内的话,自动刷新缓存
    private LoginUser checkLoginTime(LoginUser loginUser) {
        long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MINUTES_10) {
            String token = loginUser.getToken();
            loginUser = (LoginUser) userDetailsService.loadUserByUsername(loginUser.getUsername());
            loginUser.setToken(token);
            tokenService.refresh(loginUser);
        }
        return loginUser;
    }

    //根据参数或者header获取token
    public static String getToken(HttpServletRequest request) {
        String token = request.getParameter(TOKEN_KEY);
        if (StringUtils.isBlank(token)) {
            token = request.getHeader(TOKEN_KEY);
        }
        return token;
    }

}

 由于登录时,请求中并未携带token,因此请求下一步会到达具体的登录controller

package com.wensi.web.auth;

@Api(tags = "登陆管理")
@RestController
@RequestMapping("/auth")
public class LoginController {

    @ApiOperation(value = "登录")
    @PostMapping("/login")
    public Resp<Token> login(@RequestBody @Valid LoginReq loginReq){
        Token token = authService.login(loginReq);
        return Resp.success(token);
    }
	
    @ApiOperation(value = "当前用户")
    @GetMapping(path = "/current")
    public Resp<UserInfo> current() {
        LoginUser current = UserUtils.current();
        return Resp.success(current);
    }
}

然后到达service进行校验处理,该service中用到了很多SpringSecurity中的类,将认证信息set到了SecurityContext上下文,最后又调用了TokenService生成了JwtToken。

package com.wensi.auth.domain.service;

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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class AuthServiceImpl implements AuthService {

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    public Token login(LoginReq loginReq) {
        //校验用户
        UserEntity userEntity = Optional.ofNullable(userDao.findByUsername(loginReq.getUsername()))
                .orElseThrow(() -> ErrState.USER_NOT_FOUND.err(loginReq.getUsername()));
        return this.checkLogin(userEntity, loginReq.getPassword());
    }
    //登录认证
    private Token checkLogin(UserEntity userEntity, String password){
        //校验密码
        boolean flag = bCryptPasswordEncoder.matches(password, userEntity.getPassword());
        if (!flag){
            throw ErrState.PASSWORD_ERR.err();
        }
        if (null != userEntity.getStatus() && UserStatus.LOCKED.equals(userEntity.getStatus())){
            throw ErrState.ACCOUNT_LOCK.err();
        }
        if (null != userEntity.getStatus() && UserStatus.DISABLE.equals(userEntity.getStatus())){
            throw ErrState.ACCOUNT_DISABLE.err();
        }
        UsernamePasswordAuthenticationToken upToken =
                new UsernamePasswordAuthenticationToken(userEntity.getUsername(), password);
        Authentication authentication = authenticationManager.authenticate(upToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        //保存token到redis, 生成jwtToken
        UserInfo userInfo = userAssemble.userInfo(userEntity);
        LoginUser loginUser = new LoginUser();
        BeanUtils.copyProperties(userInfo, loginUser);
        return tokenService.saveToken(loginUser);
    }
}

该类主要完成token生成以及将token和当前用户信息缓存到redis 

package com.wensi.auth.domain.service;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.data.redis.core.RedisTemplate;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;

@Slf4j
@Component
public class TokenServiceJWTImpl implements TokenService {

    @Value("${token.expire.seconds}")
    private Integer expire;

    @Value("${token.jwtSecret}")
    private String jwtSecret;

    private static Key KEY = null;

    private static final String LOGIN_USER_KEY = "LOGIN_USER_KEY";

    @Autowired
    private RedisTemplate<String, LoginUser> redisTemplate;

    @Autowired
    private SysLogService sysLogService;

    @Autowired
    private HttpServletRequest request;

    @Override
    public Token saveToken(LoginUser loginUser) {
        loginUser.setToken(UUID.randomUUID().toString());
        cacheLoginUser(loginUser);
        // 登陆日志
        String ip = Optional.ofNullable(request.getHeader("X-Real-IP"))
                .filter(StringUtils::isNotBlank).orElse(request.getRemoteHost());
        SysLogReq sysLogReq = new SysLogReq(loginUser.getId(), ip,
                loginUser.getNickName(),"登录", 1, null);
        sysLogService.add(sysLogReq);
        String jwtToken = createJWTToken(loginUser);
        return new Token(jwtToken, loginUser.getLoginTime());
    }

    //缓存token到redis
    private void cacheLoginUser(LoginUser loginUser) {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expire * 1000);
        // 根据token将loginUser缓存
        redisTemplate.boundValueOps(getTokenKey(loginUser.getToken()))
                .set(loginUser, expire, TimeUnit.SECONDS);
    }

    //定制jwt令牌
    private String createJWTToken(LoginUser loginUser) {
        // 荷载部分放入token,通过该串可找到登陆用户
        Map<String, Object> claims = new HashMap<>();
        claims.put(LOGIN_USER_KEY, loginUser.getToken());
        //签发令牌
        String jwtToken = Jwts.builder().setClaims(claims)
                .signWith(SignatureAlgorithm.HS256, getKeyInstance())
                .compact();
        return jwtToken;
    }

    //定制公钥
    private Key getKeyInstance() {
        if (KEY == null) {
            synchronized (TokenServiceJWTImpl.class) {
                if (KEY == null) {
                    byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(jwtSecret);
                    KEY = new SecretKeySpec(apiKeySecretBytes, SignatureAlgorithm.HS256.getJcaName());
                }
            }
        }
        return KEY;
    }

    @Override
    public void refresh(LoginUser loginUser) {
        cacheLoginUser(loginUser);
    }

    @Override
    public LoginUser getLoginUser(String jwtToken) {
        String token = parseJWT(jwtToken);
        if (StringUtils.isNotBlank(token)) {
            return redisTemplate.boundValueOps(getTokenKey(token)).get();
        }
        return null;
    }

    //解析jwt令牌, 拿到荷载中的token
    private String parseJWT(String jwtToken) {
        if ("null".equals(jwtToken) || StringUtils.isBlank(jwtToken)) {
            return null;
        }
        try {
            Map<String, Object> jwtClaims = Jwts.parser()
                    .setSigningKey(getKeyInstance())
                    .parseClaimsJws(jwtToken).getBody();
            return MapUtils.getString(jwtClaims, LOGIN_USER_KEY);
        } catch (ExpiredJwtException e) {
            log.error("{}已过期", jwtToken);
        } catch (Exception e) {
            log.error("{}", e);
        }
        return null;
    }

    @Override
    public boolean deleteToken(String jwtToken) {
        String token = parseJWT(jwtToken);
        if (StringUtils.isNotBlank(token)) {
            String key = getTokenKey(token);
            LoginUser loginUser = redisTemplate.opsForValue().get(key);
            if (loginUser != null) {
                redisTemplate.delete(key);
                // 退出日志
                String ip = Optional.ofNullable(request.getHeader("X-Real-IP"))
                        .filter(StringUtils::isNotBlank).orElse(request.getRemoteHost());
                SysLogReq sysLogReq = new SysLogReq(loginUser.getId(), ip,
                        loginUser.getNickName(),"退出", 1, null);
                sysLogService.add(sysLogReq);
                return true;
            }
        }
        return false;
    }

    //定制redisKey
    private String getTokenKey(String token) {
        return "tokens:" + token;
    }

}

这里看一下Token、loginUser、UserInfo三者的嵌套关系

Token

token属性实际保存了生成的Jwt字符串

public class Token implements Serializable {

    private static final long serialVersionUID = -164567294469931676L;

    /**
     * token
     */
    private String token;
    /**
     * 登陆时间戳(毫秒)
     */
    private Long loginTime;

}

 LoginUser继承了UserInfo,UserInfo中保存了用户的相关信息

package com.wensi.auth.adapter.dto;

import org.springframework.security.core.userdetails.UserDetails;

public class LoginUser extends UserInfo implements UserDetails {

    @Setter
    @Getter
    private String token;

    /**
     * 登陆时间
     */
    @Setter
    @Getter
    private Long loginTime;

    /**
     * 过期时间
     */
    @Setter
    @Getter
    private Long expireTime;

    // 其他实现方法忽略
}

 UserInfo类:

package com.wensi.user.assemble.resp;

@Data
public class UserInfo {
    private Long id;
    private String username;
    private String password;
    private String nickName;
    private String headIconId;
    private String phone;
    private String email;
    private Date birthday;
    private int sex;
    private UserStatus status;
    private Date createTime;
    private Date updateTime;
    private List<RoleResp> roles;
    private List<PermissionResp> permissions;
}
  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值