转载

Shiro-兵器谱之认证

上一篇[shiro-初体验]中讲解了Shiro的简单用法, 实现了URL是否需要登录访问, 当未登录访问URL时自动跳转至登录页.

本篇主要讲解在Shiro如何实现登录处理. 先简单说一下Shiro的登录处理流程.

Shiro的登录处理是在 authc 过滤器中. authc 会判断如果是登录请求会单独处理, 因此登录请求必须要配置成 authc .

其中, 登录请求包括两个:

  • 访问登录 : 进入登录页面
  • 提交登 录: 登录页点击登录按钮发出的请求

Shiro判断是否是登录请求时认这两个登录请求必须是同一个地址, 并且 GET 为访问登录页, POST 为提交登录

// 登录请求(包括访问登录页和提交登录)
if (isLoginRequest(request, response)) {
    // 提交登录
    if (isLoginSubmission(request, response)) {
        // 提交登录, 执行Shiro的登录逻辑
        return executeLogin(request, response);
    } else {
        // 访问登录请求, 继续执行进入控制器
        return true;
    }
}
复制代码

上一篇中, 我们访问登录页的请求为 /login.jsp , 直接访问登录JSP, 如果我们在用 POST 访问JSP显然是不合理的使用JSP了.

因此, 我们将登录请求修改为 /login , 在控制器中对 GETPOST 进行处理. 当修改了登录请求地址时需要在Shiro配置一下

// Shiro核心配置
shiroFilter(ShiroFilterFactoryBean) {
    // 登录URL(包括请求登录页和提交登录)
    // 自定义的登录URL必须单独设置
    loginUrl = "/login"
    
    // ....
}
复制代码

相应的,在控制器中也增加两个方法分别处理登录请求和提交登录

// 处理请求登录页面
@GetMapping("/login")
public String toLogin() {
    return "/login";
}
 
// 处理提交登录
@PostMapping("/login")
public String login() {
    System.out.println("处理提交登录");
    return "/success";
}
复制代码

登录页面: login.jsp

<form action="/login" method="POST">
	<input type="text" name="username" placeholder="用户名" value="" />
	<input type="password" name="password" placeholder="密码" value="" />
	<input type="submit" value="立即登录" />
</form>
复制代码

完成上述操作后启动项目, 访问 /page/a 时, 由于未登录Shiro会重定向至 /login , 在登录页面输入用户名和密码, 点击立即登录按钮后会以 POST 方式提交至 /login , Shiro就会处理本次登录请求了.

  • 用户名和密码的name必须为username和password (Shiro会从Request中取这两个参数名的值作为用户名和密码)
  • 请求必须是POST, 请求地址必须和Shiro配置文件中的loginUrl保持一致.

那么, 问题来了, Shiro怎么知道输入的用户名和密码是否正确呢?

答案一定是不知道, 因此, 需要我们对用户名和密码进行验证后将结果告诉Shiro. 那么如何实现自定义验证呢?

1) 自定义Realm

Shiro对 Realm 的定义: 一个可以访问系统安全相关信息(例如用户, 角色, 权限等)的组件. 通俗的说, 就是在 Realm 实现写查询用户, 角色, 权限等系统安全相关的数据的方法.

用户信息一般会保存在数据库中, 我们可以在 Realm 中通过登录页面传递的用户名去数据库查询用户, 将结果返回给Shiro.

然而Shiro并不知道用户名和密码是否正确, 所以提供了 Realm 组件, 让我们在 Realm 中查询用户相关信息并返回, Shiro根据 Realm 返回结果判断是否登录成功.

举个例子

你在相亲的时候要请女生吃饭, 你也不知道每次相亲的女生喜欢吃什么. 但针对每个菜系都你准备好了相应的餐厅. 聪明的你准备了一个小盒子, 相亲时让女生把想吃的写好放到盒子里面, 然后你根据盒子里面的内容到事先准备好的餐厅去吃饭. 至于女生是用铅笔写的, 还是钢笔写的你根本不会关心, 你只关心女生想吃什么.

上例中的你相当于Shiro, 准备好各种餐厅相当于实现了各种登录的逻辑, 小盒子就相当于 Realm , 女生写的纸条相当于实现了一个Realm, 纸条上的内容相当于查询到的用户信息. 至于是用铅笔还是钢笔写则相当于用户信息获取方式(数据库,文件或其他).

Shiro只关心返回的结果, 不会关心Realm查询用户信息的实现过程. 下面我们来实现一个 Realm

// 自定义查询用户信息的Realm
public class UserRealm extends AuthenticatingRealm {
 
    // 获取用户信息的方法
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 登录用户名
        // Shiro会将提交登录传入的用户名和密码封装到UsernamePasswordToken中
        String username = ((UsernamePasswordToken) token).getUsername();
 
        // 根据用户名从数据库或其他存储中查询用户信息
        // 模拟数据库查询, 返回用户信息
        User dbUser = getUser(username);
 
        // 用户不存在,当返回null时Shiro会认为用信息不存在
        if (dbUser == null) {
            return null;
        }
 
        // 将查询到用户信息返回给Shiro
        // 参数1: Shiro会将该参数作为当前登录用户的信息保存,随时可取
        // 参数2: 当前用户的密码,Shiro使用该参数和提交登录传递的密码进行判断
        // 参数3: Realm名称,暂不处理
        return new SimpleAuthenticationInfo(dbUser, dbUser.getPassword(), "");
    }
 
}
复制代码
  • 继承AuthenticatingRealm并实现获取用户信息的doGetAuthenticationInfo方法
  • 根据用户名查询到用户信息时返回Shiro需要的AuthenticationInfo对象(内置多种返回对象,稍后介绍)
  • 未查询到用户时返回null, 返回结果为null时Shiro会按照用户不存在进行处理, 本次登录失败
  • 密码是否争取判断不在该方法中进行,Shiro会根据返回结果进行判断,密码正确时登录成功. 错误时本次登录失败
  • Shiro不关心获取用户信息的方式, 无论是数据库查询还是文件查询,或是第三方接口,只要按照格式返回即可.
  • Shiro会将返回结果第一个参数对象保存,登录成功后可通过Shiro的方法获取登录用户的相关信息(例获取登录用户ID等)

本例未连接数据库, 模拟代码:

// 模拟根据用户名在数据库查询用户信息
private User getUser(String username) {
    // 使用"atd681"作为登录密码才能查到信息
    if (!"atd681".equals(username)) {
        return null;
    }
 
    User dbUser = new User();
    dbUser.setUserId(1L);
    dbUser.setUsername(username);
    dbUser.setPassword("123");
 
    return dbUser;
}
复制代码
  • 有效登录用户名:atd681, 密码:123, 用户ID:1
  • 其余用户名登录失败

2) 配置Realm

自定义 Realm 后需要告知Shiro哪个 Realm 是查询用户信息的, 即将 Realm 配置到Shiro中

// 安全管理器
securityManager(DefaultWebSecurityManager) { 
    realm = ref("userRealm") 
}
 
// 定义Realm
userRealm(UserRealm)
复制代码
  • 在Shiro配置文件中定义Realm
  • 将Realm配置到安全管理器securityManager中

启动项目, 访问 /page/a , 未登录时Shiro重定向至登录页面. 输入 atd681/123 即可登录成功并跳转 /page/a

3) 配置默认成功页

当登录成功后, Shiro会重定向到成功页面

  • 当访问其他页面( /page/a )跳转至登录时, 登录成功会跳转至目标页面( /page/a )
  • 直接访问登录页(无目标页), 登录成功后跳转至默认成功页

Shiro默认成功页为 / , 可自定义默认成功页

// Shiro核心配置
shiroFilter(ShiroFilterFactoryBean) {
    // 默认登录成功后跳转的页面地址
    successUrl = "/index"
 
    // 其他配置...
}
复制代码

4) 处理登录失败

登录成功后并没有执行到控制器中的处理 POST 登录的方法. 输入 atd681 以外的账号或输入错误密码会导致登录失败, 却会执行控制器中的处理 POST 登录的方法. 为什么呢???

Shiro的登录逻辑:

Shiro-兵器谱之认证
  • 访问登录页面时, Shiro不处理, 进入控制器
  • 登录成功后, 直接重定向至成功页面(不进入控制器)
  • 登录失败时, 进入控制器处理, 由控制器决定登录失败页面

登录失败时, Shiro用异常表示失败原因, 并将失败原因保存在 Request 中, key为 shiroLoginFailure , Shiro登录逻辑中会抛出如下异常:

org.apache.shiro.authc.UnknownAccountException
org.apache.shiro.authc.IncorrectCredentialsException

同时内置了如下异常, 方便用户自行验证时抛出:

org.apache.shiro.authc.DisabledAccountException
org.apache.shiro.authc.LockedAccountException
org.apache.shiro.authc.ExcessiveAttemptsException
org.apache.shiro.authc.ConcurrentAccessException

登录失败时可以根据异常在页面中显示相应的错误提示信息, 本例登录失败时返回登录页并显示错误信息

<!-- 有登录错误信息时,根据异常显示对应的提示信息 -->
<c:if test="${shiroLoginFailure != null}">
	<c:if test="${shiroLoginFailure == 'org.apache.shiro.authc.UnknownAccountException'}">用户不存在</c:if>
	<c:if test="${shiroLoginFailure == 'org.apache.shiro.authc.IncorrectCredentialsException'}">密码不正确</c:if>
</c:if>
<!-- 无登录错误时 -->
<c:if test="${shiroLoginFailure == null}">你访问的页面需要先进行登录</c:if>
 
<form action="/login" method="post">
	<input type="text" name="username" placeholder="用户名" value="" />
	<input type="password" name="password" placeholder="密码" value="" />
	<input type="submit" value="立即登录" />
</form>
复制代码

4) 登出

配置登出URL使用 logout 过滤器即可. Shiro登出后默认重定向至登录页.

// Shiro核心配置
shiroFilter(ShiroFilterFactoryBean) {
    // 配置URL规则
    // 有请求访问时Shiro会根据此规则找到对应的过滤器处理
    filterChainDefinitionMap = [
        "/page/n" : "anon", // /page/n不需要登录即可访问
        "/logout" : "logout", // 登出使用logout过滤器
        "/**": "authc" // 其余所有页面需要认证(authc为认证过滤器)
    ]
 
    // 其他配置 ....   
}
复制代码

如登出后自定义重定向页面, 需要在配置文件中手动定义 logout 过滤器(未定义时Shiro会通过Spring自动加载)

// 手动定义Logout过滤器
// 未定义时Shiro会通过Spring自动加载
logout(LogoutFilter){
    redirectUrl = "/logout_success.jsp"
}
复制代码

同时, 必须配置 logout_success.jsp 不需要登录也可以访问( anon ), 如果不配置, 登出后进入 logout_success.jsp 不需要时会被Shiro拦截(此时未登录)并重定向至登录(登录成功后会重定向至 logout_success.jsp )

"/logout_success.jsp" : "anon", // 登出成功页不需要认证
复制代码

5) 获取登录用户信息

1) 自定义Realm 中提到获取的登录用户信息在登录成功后会被Shiro保存. Shiro提供了可以获取登录用户信息的方法.

@RequestMapping("/page/a")
public String toPageA(ModelMap map) {
    // Shiro提供的获取当前登录用户信息的静态方法
    // 用户信息对象为在Realm中保存的对象
    User user = (User) SecurityUtils.getSubject().getPrincipal();
    // 获取用户ID,用户名
    map.put("userId", user.getUserId());
    map.put("userName", user.getUsername());
 
    return "/page_a";
}
复制代码

获取到的用户对象必须和在 Realm 中返回 SimpleAuthenticationInfo 对象中第一个参数一致

至此, 基于Shiro认证的示例配置完成.

  • 示例代码地址: https://github.com/atd681/alldemo
  • 示例项目名称: atd681-shiro-authc
原文  https://juejin.im/post/5b65131c51882561ef44582d
正文到此结束
Loading...