二、登录、认证、授权

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;
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342