springboot集成shiro实现用户登录认证

近期一直在研究这个,遇到了很多坑,所以今天来记录下实现和解决的思路。

环境

  1. JDK1.8

  2. maven

  3. IDEA

    总结

    在Spring Boot中集成Shiro进行用户的认证过程主要可以归纳为以下三点:

1、定义一个ShiroConfig,然后配置SecurityManager Bean,SecurityManager为Shiro的安全管理器,管理着所有Subject;

2、在ShiroConfig中配置ShiroFilterFactoryBean,其为Shiro过滤器工厂类,依赖于SecurityManager;

3、自定义Realm实现,Realm包含doGetAuthorizationInfo()和doGetAuthenticationInfo()方法,因为本文只涉及用户认证,所以只实现doGetAuthenticationInfo()方法。

引入依赖

首先你应该看过如下文章:

1、 搭建springboot项目
2、 Hello Springboot

接下来引入我们这次需要的依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>

ShiroConfig

这是一个Shiro的配置类,创建一个ShiroConfig类

@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 设置securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 登录的url
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登录成功后跳转的url
        shiroFilterFactoryBean.setSuccessUrl("/index");
        // 未授权url
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        // 定义filterChain,静态资源不拦截
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/fonts/**", "anon");
        filterChainDefinitionMap.put("/img/**", "anon");
        // druid数据源监控页面不拦截
        filterChainDefinitionMap.put("/druid/**", "anon");
        // 配置退出过滤器,其中具体的退出代码Shiro已经替我们实现了
        filterChainDefinitionMap.put("/logout", "logout");
        filterChainDefinitionMap.put("/", "anon");
        // 除上以外所有url都必须认证通过才可以访问,未通过认证自动访问LoginUrl
        filterChainDefinitionMap.put("/**", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    @Bean
    public SecurityManager securityManager(){
        // 配置SecurityManager,并注入shiroRealm
        DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm());
        return securityManager;
    }

    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        // Shiro生命周期处理器
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public ShiroRealm shiroRealm(){
        // 配置Realm,需自己实现
        ShiroRealm shiroRealm = new ShiroRealm();
        return shiroRealm;
    }
}

如果单单引入这个类,肯定是会报错的,所以接下来我们要继续做。
另外需要注意一点:

需要注意的是filterChain基于短路机制,即最先匹配原则,例如:

1、/user/**=anon  
2、/user/aa=authc 永远不会执行

 

其中anon、authc等为Shiro为我们实现的过滤器,这些都是已经内置在框架里面,不需要我们额外的再写代码,具体如下表所示:

Filter NameClassDescription
anonorg.apache.shiro.web.filter.authc.AnonymousFilter匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例/static/**=anon
authcorg.apache.shiro.web.filter.authc.FormAuthenticationFilter基于表单的拦截器;如/**=authc,如果没有登录会跳到相应的登录页面登录
authcBasicorg.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilterBasic HTTP身份验证拦截器
logoutorg.apache.shiro.web.filter.authc.LogoutFilter退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/),示例/logout=logout
noSessionCreationorg.apache.shiro.web.filter.session.NoSessionCreationFilter不创建会话拦截器,调用subject.getSession(false)不会有什么问题,但是如果subject.getSession(true)将抛出DisabledSessionException异常
permsorg.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter权限授权拦截器,验证用户是否拥有所有权限;属性和roles一样;示例/user/**=perms[“user:create”]
portorg.apache.shiro.web.filter.authz.PortFilter端口拦截器,主要属性port(80):可以通过的端口;示例/test= port[80],如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样
restorg.apache.shiro.web.filter.authz.HttpMethodPermissionFilterrest风格拦截器,自动根据请求方法构建权限字符串;示例/users=rest[user],会自动拼出user:read,user:create,user:update,user:delete权限字符串进行权限匹配(所有都得匹配,isPermittedAll)
rolesorg.apache.shiro.web.filter.authz.RolesAuthorizationFilter角色授权拦截器,验证用户是否拥有所有角色;示例/admin/**=roles[admin]
sslorg.apache.shiro.web.filter.authz.SslFilterSSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口443;其他和port拦截器一样;
userorg.apache.shiro.web.filter.authc.UserFilter

ShiroRealm

创建一个ShiroRealm类

public class ShiroRealm extends AuthorizingRealm {
    @Autowired
    private UserMapper userMapper;

    /**
     * 获取用户角色和权限
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 获取用户输入的用户名和密码
        String userName = (String) token.getPrincipal();
        String password = new String((char[]) token.getCredentials());

        System.out.println("用户" + userName + "认证-----ShiroRealm.doGetAuthenticationInfo");
        // 通过用户名到数据库查询用户信息
        User user = userMapper.findByUsername(userName);



        if (user == null) {
            throw new UnknownAccountException("用户名或密码错误!");
        }
        if (!password.equals(user.getPassword())) {
            throw new IncorrectCredentialsException("用户名或密码错误!");
        }
        if (user.getStatus().equals("0")) {
            throw new LockedAccountException("账号已被锁定,请联系管理员!");
        }
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
        return info;
    }
}

这里我们继承了AuthorizingRealm类,然后实现了doGetAuthenticationInfo功能,由于我们只是做用户验证,所以就不把过多的篇幅放在别的地方了。
其中UnknownAccountException等异常为Shiro自带异常,Shiro具有丰富的运行时AuthenticationException层次结构,可以准确指出尝试失败的原因。你可以包装在一个try/catch块,并捕捉任何你希望的异常,并作出相应的反应。例如:

try {
    currentUser.login(token);
} catch ( UnknownAccountException uae ) { ...
} catch ( IncorrectCredentialsException ice ) { ...
} catch ( LockedAccountException lae ) { ...
} catch ( ExcessiveAttemptsException eae ) { ...
} ... catch your own ...
} catch ( AuthenticationException ae ) {
    //unexpected error?
}

注意: 虽然我们可以准确的获取异常信息,并根据这些信息给用户提示具体错误,但最安全的做法是在登录失败时仅向用户显示通用错误提示信息,例如“用户名或密码错误”。这样可以防止数据库被恶意扫描。

接下来我们就来实现数据层:

UserMapper

public interface UserMapper extends JpaRepository<User, String> {

    User findByUsername(String username);
}

User

这是实体类。

@Entity
@Table(name = "user")
public class User {
    @Id
    @GeneratedValue
    private Long id;
    private String username;
    private String password;
    private Date create_time;
    private String status;
	//get/set...
}

由于我们用的是MySQL,数据层框架用的Jpa所以,程序自动帮我们建表了,我们可以安心来实现业务逻辑。

MD5Util

我们来实现一个工具类,这个类主要帮我们生成的是MD5加密后的密码,方便后续插入数据库的操作。

public class MD5Utils {

    private static final String SALT = "bcfou";

    private static final String ALGORITH_NAME = "md5";

    private static final int HASH_ITERATIONS = 2;

    public static String encrypt(String pswd) {
        String newPassword = new SimpleHash(ALGORITH_NAME, pswd, ByteSource.Util.bytes(SALT), HASH_ITERATIONS).toHex();
        return newPassword;
    }

    public static String encrypt(String username, String pswd) {
        String newPassword = new SimpleHash(ALGORITH_NAME, pswd, ByteSource.Util.bytes(username + SALT),
                HASH_ITERATIONS).toHex();
        return newPassword;
    }
    public static void main(String[] args) {

        System.out.println(MD5Utils.encrypt("test", "123456"));
    }
}

运行之后显示的就是我们将要插入数据库的密码,帐号是不变的。SQL语句我这里就不提供了。

接下来我们开始准备前端的页面。

index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
   <meta charset="UTF-8">
   <title>首页</title>
</head>
<body>
<p>你好![<span th:text="${user.username}">username</span>] </p>
<a th:href="@{/logout}">注销</a>
</body>
</html>

login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
   <meta charset="UTF-8">
   <title>登录</title>
   <link rel="stylesheet" th:href="@{/css/login.css}" type="text/css">
   <script th:src="@{/js/jquery-3.3.1.js}"></script>
</head>
<body>
<div class="login-page">
   <div class="form">
       <input type="text" placeholder="用户名" name="username" required="required"/>
       <input type="password" placeholder="密码" name="password" required="required"/>
       <button onclick="login()">登录</button>
   </div>
</div>
</body>
<script th:inline="javascript">
   var ctx = [[@{/}]];
       function login() {
           var username = $("input[name='username']").val();
           var password = $("input[name='password']").val();
           $.ajax({
               type: "post",
               url: ctx + "login",
               data: {"username": username,"password": password},
               dataType: "json",
               success: function (r) {
                   if (r.code == 0) {
                       location.href = ctx + 'index';
                   } else {
                       alert(r.msg);
                   }
               }
           });
       }
</script>
</html>

最后我们来实现Controller

LoginController

创建一个LoginController类

@Controller
public class LoginController {

    @GetMapping("/login")
    public String login() {
        return "login";
    }
    @PostMapping("/login")
    @ResponseBody
    public ResponseBo login(String username, String password) {
        // 密码MD5加密
        password = MD5Utils.encrypt(username, password);
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        // 获取Subject对象
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
            return ResponseBo.ok();
        } catch (UnknownAccountException e) {
            return ResponseBo.error(e.getMessage());
        } catch (IncorrectCredentialsException e) {
            return ResponseBo.error(e.getMessage());
        } catch (LockedAccountException e) {
            return ResponseBo.error(e.getMessage());
        } catch (AuthenticationException e) {
            return ResponseBo.error("认证失败!");
        }
    }
    @RequestMapping("/")
    public String redirectIndex() {
        return "redirect:/index";
    }
    @RequestMapping("/index")
    public String index(Model model) {
        // 登录成后,即可通过Subject获取登录的用户信息
        User user = (User) SecurityUtils.getSubject().getPrincipal();
        model.addAttribute("user", user);
        return "index";
    }
}

这些做完,我们启动测试。

测试

启动项目以后,我们无论访问什么路径都会被重定向到:
http://localhost:8080/login
图1
当我们输入错误的帐号、密码时:
图2
当我们输入正确的帐号、密码时:
图3
由于我们将test账户的状态改成了未激活的状态,所以这里会显示已被锁定。
接下来输入正确的帐号、密码:
图4
这样我们就来到了主页。
点击注销,就会返回登录页面。

小结

这个小功能整整折腾了2天才弄出来,因为之前完全没接触过shiro,所以去大概浏览下相关的教程,然后开始跟springboot整合,中间遇到了很多问题,比如:
1、Jquery的版本用的不对,导致前台数据没有传到后台,换了高版本的解决了。
2、Jpa框架的Mapper(由于之前一直用Mybatis,所以叫习惯了,Jpa习惯叫Repository),由于方法:findByUsername写成了findByUserName,导致程序出错,我都不晓得怎么肥四。。emmm不过看报错日志,就是这个问题改了就能跑通了。
3、密码的问题,看了很多资料都没有说密码怎么来的,所以就自己搜索shiro的加密方式,然后做了MD5加密工具来生成密码。(这里我是一度要放弃的,准备用老方法进行加解密的,跳了很多坑)。
4、前端的问题,这些就是小问题了,最后通过查阅Thymeleaf的文档解决了。

最后

本文源码:Github

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

Copyright bcfou.com.Some Rights Reserved.