二、登录、认证、授权

1. JWT、Spring Security

    若依(分离版)的登录、认证、授权使用到了JWT、Spring Security,下面先通过例子对JWT、Spring Security的用法进行介绍。

1.1 JWT
1.1.1 JWT简介

    JWT全称JSON Web Token,由header、payload、signature这三部分组成,通过数字签名方式,以JSON对象为载体,在不同服务终端间安全得传输信息。JWT最常见的应用场景就是授权认证,用户登录后,会用JWT对生成的token进行加密,后续每个请求的请求头中都要携带加密后的值,系统在接收请求时,会用JWT进行解密,得到token,进行后续操作。

1.1.2 JWT案例
    public static void main(String[] args) {
        // JWT加密
        String jwtStr = Jwts.builder()
                // header部分
                .setHeaderParam("type", "JWT")
                .setHeaderParam("alg", "HS512")
                // payload部分
                .claim("user_name", "zs")
                .claim("age", "20")
                .setSubject("jwt_test")
                .setId(IdUtils.fastUUID())
                // signature
                .signWith(SignatureAlgorithm.HS512, "abcdefghijklmnopqrstuvwxyz")
                // 调用compact对这三部分进行处理,得到一个安全的JWT字符串
                .compact();
        System.out.println(jwtStr);
        // jwtStr:
        // eyJ0eXBlIjoiSldUIiwiYWxnIjoiSFM1MTIifQ
        // .eyJ1c2VyX25hbWUiOiJ6cyIsImFnZSI6IjIwIiwic3ViIjoiand0X3Rlc3QiLCJqdGkiOiJmZmIyNTg3My1lYTI2LTRkNTItOTVhYy1mMzgxODRkMWFkMjYifQ
        // .jlR-tUzSFnqSq8zcJt8LqM5kuthoSrPr4HygbVjWu19w6CEG7WEAHf0qReSbRPp-i4FXi1K9x_s6XYZv_OKhrg

        // JWT解密
        Claims claims = Jwts.parser()
                .setSigningKey("abcdefghijklmnopqrstuvwxyz")
                .parseClaimsJws(jwtStr)
                .getBody();
        System.out.println(claims.get("user_name")); // zs
        System.out.println(claims.get("sub")); // jwt_test
        System.out.println(claims.getSubject()); // jwt_test
    }
1.2 Spring Security应用案例简介
1.2.1 自定义用户认证逻辑

    要进行账号密码校验,需要实现Spring Security提供的UserDetailsService和UserDetails接口、重写loadUserByUsername方法、返回UserDetails。
(1)UserDetailsServiceImpl

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private ISysUserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {
        // 通过登录页面中传入的用户名(页面中可能传入错误的用户名),从数据库查询用户信息
        SysUser user = userService.selectUserByUserName(username);
        
        // 自定义校验(略)

        // 创建实现了UserDetails的LoginUser对象
        return new LoginUser(user);
    }

}

(2)LoginUser

public class LoginUser implements UserDetails {

    private static final long serialVersionUID = 1L;

    /**
     * 用户信息
     */
    private SysUser user;

    public LoginUser() {
    }

    public LoginUser(SysUser user, Set<String> permissions) {
        this.user = user;
    }

    public SysUser getUser() {
        return user;
    }

    public void setUser(SysUser user) {
        this.user = user;
    }

    // 重写UserDetails的getPassword、getUsername方法
    // Spring Security会调用这getPassword校验密码是否正确
    @Override
    public String getPassword() {
        return user.getPassword();
    }
    @Override
    public String getUsername() {
        // 测试发现,这里return null;登录时也不会报错,说明Spring Security只对密码进行校验
        return user.getUserName();
    }

    // 其他方法略

}
1.2.2 登录时校验密码
    // 这里的username和password是从页面传入的
    public String login(String username, String password) {
        boolean captchaOnOff = configService.selectCaptchaOnOff();
        Authentication authentication = null;
        try {
            // 该方法会调用到UserDetailsServiceImpl.loadUserByUsername(从数据库中查出真实
            // 密码,并设置到LoginUser中,后续的过滤器会用真实的密码与这里从页面中传入的密
            // 码进行对比,如果不一致,会抛出BadCredentialsException异常)
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
        } catch (Exception e) {
            if (e instanceof BadCredentialsException) {
                // 捕获BadCredentialsException异常,抛出自定义异常
                throw new UserPasswordNotMatchException();
            } else {
                throw new ServiceException(e.getMessage());
            }
        }
    }
1.2.3 自定义过滤器

    Spring Security框架是由很多个过滤器组成的,需要自定义一个过滤器,在每次请求时,将UsernamePasswordAuthenticationToken设置到SecurityContextHolder,供后面的过滤器认证时使用。

// OncePerRequestFilter:过滤器基类,目的是保证在任何servlet容器上,每次请求调度都能执行一次
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException
    {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        // 设置authenticationToken,后面的过滤器会通过authenticationToken判断是否认证
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        chain.doFilter(request, response);
    }
}
1.2.4 授权

    前端可以通过菜单、按钮的有无对权限进行控制,但前端的控制安全性较低,我们可以使用Spring Security提供的@PreAuthorize对后端权限进行控制。
(1)使用Spring Security提供的权限校验方法进行权限控制

    // hasAuthority是Spring Security提供的权限校验方法,该方法中会获取登录用户的权限集合,并与
    // 传入的'system:config:list'进行匹配,如果匹配成功,表示登录用户拥有权限;反之,则没有权限
    @PreAuthorize("hasAuthority('system:config:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysConfig config) {
          // 略
    }

(2)自定义权限校验方法进行权限控制

// 自定义权限控制类
@Service("permissionService ")
public class PermissionService {

    // 自定义权限校验方法(返回true:有权限,返回false:没有权限)
    public boolean hasPermi(String permission) {
        // 权限校验逻辑(略)
    }

}
    // 使用SpringEL表达式调用hasPermi方法进行权限校验
    @PreAuthorize("@permissionService .hasPermi('system:config:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysConfig config)
    {
        // 略
    }

2. 完整的登录流程

2.1 找到login.vue中的handleLogin
2.2 找到login方法



2.3 通过"/login"找到后端的controller方法

(1)SysLoginController.login

    @PostMapping("/login")
    public AjaxResult login(@RequestBody LoginBody loginBody)
    {
        AjaxResult ajax = AjaxResult.success();
        // 生成经过JWT加密的token
        String token = loginService.login(loginBody.getUsername(),
                loginBody.getPassword(), loginBody.getCode(), loginBody.getUuid());
        // 将token设置到响应体中,后续每次请求的请求头都要携带该token
        // 在请求JwtAuthenticationTokenFilter过滤器中会通过JWT对该token进行解密,
        // 并用解密后的token从Redis中获取loginUser,进行后续操作
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }

(2)SysLoginService.login

    public String login(String username, String password, String code, String uuid)
    {
        boolean captchaOnOff = configService.selectCaptchaOnOff();
        // 验证码开关
        if (captchaOnOff)
        {
            // 校验验证码
            validateCaptcha(username, code, uuid);
        }
        // 用户验证
        Authentication authentication = null;
        try
        {
            // 该方法会调用到UserDetailsServiceImpl.loadUserByUsername(从数据库中查出真实
            // 密码,并设置到LoginUser中,后续的过滤器会用真实的密码与这里从页面中传入的密码进行
            // 对比,如果不一致,会抛出BadCredentialsException异常)
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
        }
        catch (Exception e)
        {
            if (e instanceof BadCredentialsException)
            {
                // 记录登录失败日志
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                // 捕获BadCredentialsException,抛出自定义异常,
                // 异常信息为:用户不存在/密码错误
                throw new UserPasswordNotMatchException();
            }
            else
            {
                // 记录登录失败日志
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
                throw new ServiceException(e.getMessage());
            }
        }
        // 记录登录成功日志
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        // 获取authentication中保存的loginUser(上面创建authentication时,
        // 会调用到UserDetailsServiceImpl.loadUserByUsername,并将该方
        // 法返回的UserDetail(即LoginUser)设置到authentication中)
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        // 记录用户的登录信息
        recordLoginInfo(loginUser.getUserId());
        // 生成经过JWT加密的token
        return tokenService.createToken(loginUser);
    }

(3)TokenService.createToken

    public String createToken(LoginUser loginUser)
    {
        // 利用IdUtils生成token,并将生成的token设置到loginUser中
        String token = IdUtils.fastUUID();
        loginUser.setToken(token);
        // 将用户代理信息设置到loginUser中
        setUserAgent(loginUser);
        // 设置/刷新token
        refreshToken(loginUser);
        // 创建JWT的有效载荷部分(payload部分)
        Map<String, Object> claims = new HashMap<>();
        claims.put(Constants.LOGIN_USER_KEY, token);
        // 通过JWT加密token
        return createToken(claims);
    }

(4)TokenService.refreshToken

    public void refreshToken(LoginUser loginUser)
    {
        // 将登录时间戳、token过期时间戳设置到loginUser中
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 获取Redis的键
        String userKey = getTokenKey(loginUser.getToken());
        // 将loginUser设置到Redis中,过期时长是30(expireTime为30)分钟
        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }

(5)TokenService.createToken

    private String createToken(Map<String, Object> claims)
    {
        String token = Jwts.builder()
                // 设置JWT的payload
                .setClaims(claims)
                // 设置JWT的signature
                // 通过secret加密(secret配置在application.yml中)
                .signWith(SignatureAlgorithm.HS512, secret)
                // 调用compact对这几部分进行处理,得到一个安全的JWT字符串
                .compact();
        // 返回加密后的token
        return token;
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。