Shiro-认证

0) 前言

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

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

Shiro的登陆处理是在authc过滤器中. authc会判断若是是登陆请求会单独处理, 所以登陆请求必需要配置成authc.数据库

其中, 登陆请求包括两个:apache

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

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了.bash

所以, 咱们将登陆请求修改成/login, 在控制器中对GETPOST进行处理. 当修改了登陆请求地址时须要在Shiro配置一下app

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

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

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

登陆页面: login.jspide

<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就会处理本次登陆请求了.post

  • 用户名和密码的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用异常表示失败缘由, 并将失败缘由保存在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>
复制代码

5) 登出

配置登出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", // 登出成功页不须要认证
复制代码

6) 获取登陆用户信息

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对象中第一个参数一致

7) 示例代码

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

相关文章
相关标签/搜索