(六)Spring Boot整合Shiro
0 Views spring-boot with
本文字数:1,708 字 | 阅读时长 ≈ 8 min

(六)Spring Boot整合Shiro

0 Views spring-boot with
本文字数:1,708 字 | 阅读时长 ≈ 8 min

Spring Boot整合Shiro

:tada: :tada: :tada: 这里有丰富的 Spring 框架学习案例

仓库地址:spring-learn
欢迎star、fork,给作者一些鼓励

写在前面

之前有写过SSM整合Shiro的示例:SSM权限管理示例

而在Spring Boot中使用Shiro,就是需要把之前SSM的XML配置转换成Java代码配置,下面我举例用Spring Boot2.x + Shiro实现登录认证。

导入依赖

<!-- shiro -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>

由于Shiro本身并没有提供缓存实现,这里使用Shiro官方支持的ehcache缓存:

Ehcache:

<!-- ehcache缓存 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.4.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

如果你想用Redis缓存,可以用这个封装好的插件:

Shiro-redis

<dependency>
    <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis</artifactId>
    <version>2.4.2.1-RELEASE</version>
</dependency>

修改application.yml

  datasource:
    name: springboot
    type: com.alibaba.druid.pool.DruidDataSource
    #druid相关配置
    druid:
      #mysql驱动
      driver-class-name: com.mysql.cj.jdbc.Driver
      #基本属性
      url: jdbc:mysql://127.0.0.1:3306/springboot_shiro?useUnicode=true&characterEncoding=UTF-8
      username: root
      password: root

更多的配置请看该项目下src/main/resources/application.yml

初始化数据库

-- create database springboot_shiro charset utf8;

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `username` varchar(255) DEFAULT NULL COMMENT '用户名',
  `password` varchar(255) DEFAULT NULL COMMENT '密码',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

创建ShiroConfig.java

如之前在SSM整合Shiro框架时,Shiro的基础配置一般有如下:

于是,我们就大概知道ShiroConfig.java中大概需要配置什么信息了:

@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
        filterFactoryBean.setSecurityManager(securityManager);
        filterFactoryBean.setLoginUrl("/login");

        //自定义拦截器链
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/logout", "logout");
        filterChainDefinitionMap.put("/login", "anon");

        //静态资源,对应`/resources/static`文件夹下的资源
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/lib/**", "anon");

        //其他请求一律拦截,一般放在拦截器链的最后
        //区分`user`和`authc`拦截器区别:`user`拦截器允许登录用户和RememberMe的用户访问
        filterChainDefinitionMap.put("/**", "user");

        filterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return filterFactoryBean;
    }

    @Bean
    public Realm realm() {
        return new AuthRealm();
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm());
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }

    @Bean
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setGlobalSessionTimeout(60 * 60 * 10); //10分钟
        sessionManager.setSessionDAO(new EnterpriseCacheSessionDAO());
        return sessionManager;
    }
}

自定义Realm实现

上面是一个最基本的Shiro环境配置,其实这个XML中配置基本雷同的,相信你也发现了。

下面进行第二部:自定义Realm实现,创建AuthRealm.java

public class AuthRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    /**
     * 权限校验相关
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    /**
     * 身份认证相关
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        /**
         * 1. 从Token中获取输入的用户名密码
         * 2. 通过输入的用户名查询数据库得到密码
         * 3. 调用Authentication进行密码校验
         */

        //获取用户名密码
        String username = (String) authenticationToken.getPrincipal();
        String password = new String((char[]) authenticationToken.getCredentials());

        User user = userService.findByUsername(username);
        if (user == null) {
            throw new UnknownAccountException();
        }
        if (!password.equals(user.getPassword())) {
            throw new IncorrectCredentialsException();
        }
        return new SimpleAuthenticationInfo(user, password, getName());
    }
}

对于自定义Realm实现,我们仅需要继承AuthorizingRealm,看源码发现:

public abstract class AuthorizingRealm extends AuthenticatingRealm implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware {}

继承了一个抽象类,就应该实现重写它的抽象方法:

protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection var1);

protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken var1) throws AuthenticationException;

案例

上面配置好了Shiro环境,下面我们实践一下。

创建index.htmllogin.html两个页面:

创建LoginController.java,编写路由导航地址

@Controller
public class LoginController {
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 首页地址
     *
     * @return
     */
    @GetMapping(value = {"/", "/index"})
    public String index() {
        return "index";
    }

    /**
     * 登录地址
     *
     * @return
     */
    @GetMapping("/login")
    public String login() {
        return "login";
    }
}

注意几点:

启动项目,在浏览器上访问localhost:8080或者localhost:8080/index发现页面均会跳转到/login这个请求上:

细心地你会发现请求地址中可能会拼接一个JSESSIONID,并且所有的的请求中均会携带一个Cookie= JSESSIONID。这其实是Shiro用于身份验证用的,Shiro默认生成一个会话ID,并储存在Cookie中,这样浏览器每次的请求头中都将携带这个Cookie数据,Shiro拦截请求,发现这个Cookie值是有效的会话(Session) ID,就判定这个请求是合法的请求,然后再根据自定义拦截器链决定是否对该请求放行。

登录

编写一个form表单

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录页</title>
    <link rel="stylesheet" th:href="@{/css/login.css}"/>
</head>
<body>

<h1>登录页</h1>

<form method="post" action="/login">
    <input type="text" name="username"/><br/>
    <input type="password" name="password"><br/>
    <input type="submit" value="登录">
</form>

<div class="info" th:text="${info}"></div>

</body>
</html>

编写后台接口 post /login

/**
 * 登录接口
 *
 * @param username 用户名
 * @param password 密码
 * @return 状态信息或成功页面视图地址
 */
@PostMapping("/login")
public String login(String username, String password, Model model) {
    String info = null;

    //封装Token信息=用户名+密码
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    //获取Shiro Subject实例
    Subject subject = SecurityUtils.getSubject();
    try {
        subject.login(token);
        info = String.valueOf(subject.isAuthenticated());
        model.addAttribute("info", "登录状态 ==> " + info);
        return "/index";
    } catch (UnknownAccountException e) {
        e.printStackTrace();
        info = "未知账户异常";
    } catch (AuthenticationException e) {
        e.printStackTrace();
        info = "账户名或密码错误";
    } catch (Exception e) {
        e.printStackTrace();
        info = "其他异常";
    }
    model.addAttribute("info", "登录状态 ==> " + info);
    logger.info("登录状态 ==> {}", info);
    return "/login";
}

如上,前台form表单中的action="/login"method="post"决定了请求走这个地址,通过调用subject.login(token),Shiro自动查询Realm实现,于是找到我们自定义的Realm实现:AuthRealm,进而通过SimpleAuthenticationInfo方法验证了登录用户的身份,如果身份认证成功,就return "/index",否则就return "/login"

上面出现了两个/login接口:

@GetMapping("/login")
public String login() {
    return "login";
}
@PostMapping("/login")
public String login(String username, String password, Model model) {}

这里就提现出了@GetMapping@PostMapping的优势,利用Java的方法重载创建了两个名称相同的接口,但是根据HTTP请求方法的不同(Get还是Post)会自动寻找对应的映射方法。

更多的Shiro特性可以参看我的这个项目:SSM权限管理示例

同时推荐大家阅读张开涛老师的:跟我学Shiro


交流

以上仅是个人的见解,可能有些地方是错误的,深知自己的菜鸡技术,欢迎大佬指出。

个人建了一个Java交流群:671017003。 欢迎大佬或是新人入驻一起交流学习Java技术。


联系

If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.

如果你觉得这篇文章帮助到了你,你可以帮作者买一杯果汁表示鼓励