本文是接着上篇博客写的: Spring boot 入门 (三):SpringBoot 集成结合 AdminLTE(Freemarker), 利用 generate 自动生成代码, 利用 DataTable 和 PageHelper 进行分页显示. 按照前面的博客, 已经可以搭建一个简单的 Spring Boot 系统, 本篇博客继续对此系统进行改造, 主要集成了 Shiro 权限认证框架, 关于 Shiro 部分, 在本人之前的博客(认证与 Shiro 安全框架 https://www.cnblogs.com/dz-boss/p/9236083.html ) 有介绍到, 这里就不做累赘的介绍.
此系列的博客为实践部分, 以代码和搭建系统的过程为主, 如遇到专业名词, 自行查找其含义.
1.Shiro 配置类
系统搭建到目前为止, 主要用到了 3 个配置类, 均与 Shiro 有关, 后期随着项目的扩大, 配置文件也会随之增多.
FreeMarkerConfig: 主要针对 FreeMarker 页面显示的配置, 关于 Shiro 部分, 为 Shiro 标签设置了共享变量
, 如果不设置此变量, FreeMarker 页面将不能识别 Shiro 的标签
, 其主要代码如下:
configuration.setSharedVariable("shiro", new ShiroTags());
MShiroFilterFactoryBean: 设置了过滤器, 当然也可以在 Config 文件里面配置过滤器, 其缺点是:
在每次请求里面都做了 session 的读取和更新访问时间等操作, 这样在集群部署 session 共享的情况下, 数量级的加大了处理量负载
. 本项目后期将用到分布式, 因此这里就直接将过滤器与 Config 配置文件分离, 提高效率.
- private final class MSpringShiroFilter extends AbstractShiroFilter {
- protected MSpringShiroFilter(webSecurityManager webSecurityManager, FilterChainResolver resolver) {
- super();
- if (webSecurityManager == null) {
- throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
- }
- setSecurityManager(webSecurityManager);
- if (resolver != null) {
- setFilterChainResolver(resolver);
- }
- }
- @Override
- protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse,
- FilterChain chain) throws ServletException, IOException {
- HttpServletRequest request = (HttpServletRequest) servletRequest;
- String str = request.getRequestURI().toLowerCase();
- boolean flag = true;
- int idx = 0;
- if ((idx = str.indexOf("."))> 0) {
- str = str.substring(idx);
- if (ignoreExt.contains(str.toLowerCase()))
- flag = false;
- }
- if (flag) {
- super.doFilterInternal(servletRequest, servletResponse, chain);
- } else {
- chain.doFilter(servletRequest, servletResponse);
- }
- }
- }
ShiroConfiguration: 通用配置文件, 此配置文件为 Shiro 的基础通用配置文件, 只要是集成 Shiro, 必有此文件, 主要配置 Shiro 的登录认证相关的信息, 其代码如下:
- /**
- * 设置 shiro 的缓存, 缓存参数均配置在 xml 文件中
- * @return
- */
- @Bean
- public EhCacheManager getEhCacheManager() {
- EhCacheManager em = new EhCacheManager();
- em.setCacheManagerConfigFile("classpath:ehcache/ehcache-shiro.xml");
- return em;
- }
- /**
- * 凭证匹配器
- * (由于我们的密码校验交给 Shiro 的 SimpleAuthenticationInfo 进行处理了
- * 所以我们需要修改下 doGetAuthenticationInfo 中的代码;
- * )
- * @return
- */
- @Bean
- public HashedCredentialsMatcher hashedCredentialsMatcher(){
- HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
- hashedCredentialsMatcher.setHashAlgorithmName("md5");// 散列算法: 这里使用 MD5 算法;
- hashedCredentialsMatcher.setHashIterations(1);// 散列的次数, 比如散列两次, 相当于 md5(md5(""));
- return hashedCredentialsMatcher;
- }
- /**
- *
- * 主文件
- */
- @Bean(name = "myShiroRealm")
- public UserRealm myShiroRealm(EhCacheManager cacheManager) {
- UserRealm realm = new UserRealm();
- realm.setCacheManager(cacheManager);
- realm.setCredentialsMatcher(hashedCredentialsMatcher());
- return realm;
- }
- // 会话 ID 生成器
- @Bean(name = "sessionIdGenerator")
- public JavaUuidSessionIdGenerator javaUuidSessionIdGenerator(){
- JavaUuidSessionIdGenerator javaUuidSessionIdGenerator = new JavaUuidSessionIdGenerator();
- return javaUuidSessionIdGenerator;
- }
- @Bean(name = "sessionIdCookie")
- public SimpleCookie getSessionIdCookie(){
- SimpleCookie sessionIdCookie = new SimpleCookie("sid");
- sessionIdCookie.setHttpOnly(true);
- sessionIdCookie.setMaxAge(-1);
- return sessionIdCookie;
- }
- /*<!-- 会话 DAO -->*/
- @Bean(name = "sessionDAO")
- public EnterpriseCacheSessionDAO enterpriseCacheSessionDAO(){
- EnterpriseCacheSessionDAO sessionDao = new EnterpriseCacheSessionDAO();
- sessionDao.setSessionIdGenerator(javaUuidSessionIdGenerator());
- sessionDao.setActiveSessionsCacheName("shiro-activeSessionCache");
- return sessionDao;
- }
- @Bean(name = "sessionValidationScheduler")
- public ExecutorServiceSessionValidationScheduler getExecutorServiceSessionValidationScheduler() {
- ExecutorServiceSessionValidationScheduler scheduler = new ExecutorServiceSessionValidationScheduler();
- scheduler.setInterval(1800000);
- return scheduler;
- }
- @Bean(name = "sessionManager")
- public DefaultWebSessionManager sessionManager(EnterpriseCacheSessionDAO sessionDAO){
- DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
- sessionManager.setGlobalSessionTimeout(1800000);
- sessionManager.setDeleteInvalidSessions(true);
- sessionManager.setSessionValidationSchedulerEnabled(true);
- sessionManager.setSessionValidationScheduler(getExecutorServiceSessionValidationScheduler());
- sessionManager.setSessionDAO(sessionDAO);
- sessionManager.setSessionIdCookieEnabled(true);
- sessionManager.setSessionIdCookie(getSessionIdCookie());
- return sessionManager;
- }
- @Bean(name = "lifecycleBeanPostProcessor")
- public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
- return new LifecycleBeanPostProcessor();
- }
- @Bean
- public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
- DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator();
- daap.setProxyTargetClass(true);
- return daap;
- }
- @Bean(name = "securityManager")
- public DefaultWebSecurityManager getDefaultWebSecurityManager(UserRealm myShiroRealm, DefaultWebSessionManager sessionManager) {
- DefaultWebSecurityManager dwsm = new DefaultWebSecurityManager();
- dwsm.setRealm(myShiroRealm);
- // <!-- 用户授权 / 认证信息 Cache, 采用 EhCache 缓存 -->
- dwsm.setCacheManager(getEhCacheManager());
- dwsm.setSessionManager(sessionManager);
- return dwsm;
- }
- /**
- * 开启 shiro aop 注解支持.
- * 使用代理方式; 所以需要开启代码支持;
- * @param securityManager
- * @return
- */
- @Bean
- public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
- AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
- aasa.setSecurityManager(securityManager);
- return aasa;
- }
- /**
- * ShiroFilter<br/>
- * 注意这里参数中的 StudentService 和 IScoreDao 只是一个例子, 因为我们在这里可以用这样的方式获取到相关访问数据库的对象,
- * 然后读取数据库相关配置, 配置到 shiroFilterFactoryBean 的访问规则中. 实际项目中, 请使用自己的 Service 来处理业务逻辑.
- *
- */
- @Bean(name = "shiroFilter")
- public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
- ShiroFilterFactoryBean shiroFilterFactoryBean = new MShiroFilterFactoryBean();
- // 必须设置 SecurityManager
- shiroFilterFactoryBean.setSecurityManager(securityManager);
- // 如果不设置默认会自动寻找 Web 工程根目录下的 "/login.jsp" 页面
- shiroFilterFactoryBean.setLoginUrl("/login");
- // 登录成功后要跳转的连接
- shiroFilterFactoryBean.setSuccessUrl("/certification");
- //shiroFilterFactoryBean.setSuccessUrl("/index");
- shiroFilterFactoryBean.setUnauthorizedUrl("/403");
- loadShiroFilterChain(shiroFilterFactoryBean);
- return shiroFilterFactoryBean;
- }
- /**
- * 加载 shiroFilter 权限控制规则(从数据库读取然后配置)
- *
- */
- private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean){
- /////////////////////// 下面这些规则配置最好配置到配置文件中 ///////////////////////
- Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
- // authc: 该过滤器下的页面必须验证后才能访问, 它是 Shiro 内置的一个拦截器 org.apache.shiro.Web.filter.authc.FormAuthenticationFilter
- filterChainDefinitionMap.put("/login", "authc");
- filterChainDefinitionMap.put("/logout", "logout");
- // anon: 它对应的过滤器里面是空的, 什么都没做
- logger.info("################## 从数据库读取权限规则, 加载到 shiroFilter 中 ##################");
- // filterChainDefinitionMap.put("/user/edit/**", "authc,perms[user:edit]");// 这里为了测试, 固定写死的值, 也可以从数据库或其他配置中读取
- shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
- }
2. 登录认证与权限管理
主要重写了 Realm 域, 完成权限认证和权限管理:
- protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
- // 如果没有做权限验证, 此处只需要 return null 即可
- SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
- String userName = (String) principals.getPrimaryPrincipal();
- Result<TUser> list = userService.getUserByUsername(userName);
- if(list.isStatus()) {
- // 获取该用户所属的角色
- Result<List<TRole>> resultRole = roleService.getRoleByUserId(list.getResultData().getUserId());
- if(resultRole.isStatus()) {
- HashSet<String> role = new HashSet<String>();
- for(TRole tRole : resultRole.getResultData()) {
- role.add(tRole.getRoleId()+"");
- }
- // 获取该角色拥有的权限
- Result<List<TPermission>> resultPermission = permissionService.getPermissionsByRoleId(role);
- if(resultPermission.isStatus()) {
- HashSet<String> permissions = new HashSet<String>();
- for(TPermission tPermission : resultPermission.getResultData()) {
- permissions.add(tPermission.getPermissionsValue());
- }
- System.out.println("权限:"+permissions);
- authorizationInfo.setStringPermissions(permissions);
- }
- }
- }
- //return null;
- return authorizationInfo;
- }
- @Override
- protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
- // 认证登录
- String username = (String) authenticationToken.getPrincipal();
- //String password = new String((char[]) authenticationToken.getCredentials());
- Result<TUser> result = userService.getUserByUsername(username);
- if (result.isStatus()) {
- TUser user = result.getResultData();
- return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName());
- }
- //return new SimpleAuthenticationInfo(user., "123456", getName());
- return null;
- }
- }
2.1. 登录认证
首先创建一个前端登录界面, 做一个简单的登录 Form 表单
点击登录即想后台发送一个请求,
必须是 Post 请求, 否则 Shiro 不能识别
, 认证部分主要在 Ream 中完成, 新建一个类, 继承 AuthorizingRealm , 然后在重写 doGetAuthenticationInfo 方法:
只需要通过界面上的用户名查找到数据库存储的相关信息即可, 具体的认证是 Shiro 内部自己完成的, 我们只需要传入数据库中存储的用户名和密码个认证函数即可(
new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName())
), 我们可以自己重新定义密码比较器, 密码比较器的写法较多, 在认证与 Shiro 安全框架 https://www.cnblogs.com/dz-boss/p/9236083.html 中, 直接将密码比较器写入到 Ream 中, 耦合度太高, 本项目通过配置的方式重写密码比较器, 具体代码请参考参考 ShiroConfiguration 配置类:
在具体的 Login 方法中, 写入一些登录失败的异常即可, 主要用户将此失败结果存入 Session, 并显示在页面上:
- @RequestMapping(value = "/login", method = RequestMethod.POST)
- public String postLogin(RedirectAttributes redirectAttributes, HttpServletRequest request, HttpSession session) {
- // 登录失败从 request 中获取 shiro 处理的异常信息.
- // shiroLoginFailure: 就是 shiro 异常类的全类名.
- String exception = (String) request.getAttribute("shiroLoginFailure");
- System.out.println("exception=" + exception);
- String msg = "";
- if (exception != null) {
- if (UnknownAccountException.class.getName().equals(exception)) {
- System.out.println("UnknownAccountException --> 账号不存在:");
- msg = "用户不存在!";
- } else if (IncorrectCredentialsException.class.getName().equals(exception)) {
- System.out.println("IncorrectCredentialsException --> 密码不正确:");
- msg = "密码不正确!";
- } else if ("kaptchaValidateFailed".equals(exception)) {
- System.out.println("kaptchaValidateFailed --> 验证码错误");
- msg = "验证码错误!";
- } else {
- //msg = "else>>"+exception;
- msg = "密码不正确!";
- System.out.println("else -->" + exception);
- }
- }
- redirectAttributes.addFlashAttribute("msg", msg);
- session.setAttribute("msg", msg);
- //return redirect("/login");
- return "redirect:login";
- //return msg;
- }
此时登录认证部门已经完成: 一个页面 + 后台 2 个函数(1 个认证函数 + 1 个 Login 函数)
2.2. 权限管理
总体来说, 权限管理只需要在界面增加 Shiro 的权限标签即可, 可以使用角色的标签, 也可以使用权限的标签, 一般情况下 2 种标签配合使用, 效果最好 <@shiro.hasPermission name="xtgl-yhgl:read"> <@shiro.hasRolen name="xtgl-yhgl:read">
此外, 在 Realm 中, 需要重写权限认证的业务逻辑, 通常情况下通过用户 ID 找到该用户所属的角色, 然后通过角色 ID 找到该角色拥有的权限, 并将角色或者权限写入的 Shiro 中即可:
- authorizationInfo.setStringPermissions(permissions);
- authorizationInfo.setRoles(role);
本项目也是通过此逻辑完成权限管理的
上面 2 张截图表示的是一个函数.
到此, Spring Boot 集成 Shiro 框架的权限认证已经搭建完毕, 可以实现简单的权限管理.
3. 新增文件
较上一篇博客, Shiro 部分新增加的文件
来源: https://juejin.im/post/5c77dc32e51d45216e2bea42