上一篇[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
, 在控制器中对 GET
和 POST
进行处理. 当修改了登录请求地址时需要在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就会处理本次登录请求了.
那么, 问题来了, Shiro怎么知道输入的用户名和密码是否正确呢?
答案一定是不知道, 因此, 需要我们对用户名和密码进行验证后将结果告诉Shiro. 那么如何实现自定义验证呢?
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(), ""); } } 复制代码
本例未连接数据库, 模拟代码:
// 模拟根据用户名在数据库查询用户信息 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; } 复制代码
自定义 Realm
后需要告知Shiro哪个 Realm
是查询用户信息的, 即将 Realm
配置到Shiro中
// 安全管理器 securityManager(DefaultWebSecurityManager) { realm = ref("userRealm") } // 定义Realm userRealm(UserRealm) 复制代码
启动项目, 访问 /page/a
, 未登录时Shiro重定向至登录页面. 输入 atd681/123
即可登录成功并跳转 /page/a
当登录成功后, Shiro会重定向到成功页面
/page/a
)跳转至登录时, 登录成功会跳转至目标页面( /page/a
) Shiro默认成功页为 /
, 可自定义默认成功页
// Shiro核心配置 shiroFilter(ShiroFilterFactoryBean) { // 默认登录成功后跳转的页面地址 successUrl = "/index" // 其他配置... } 复制代码
登录成功后并没有执行到控制器中的处理 POST
登录的方法. 输入 atd681
以外的账号或输入错误密码会导致登录失败, 却会执行控制器中的处理 POST
登录的方法. 为什么呢???
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> 复制代码
配置登出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", // 登出成功页不需要认证 复制代码
在 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认证的示例配置完成.