SpringBoot - 整合Shiro

一、引入相关依赖

后面两个依赖可以不引入,还没有使用过Redis来做Shiro的缓存。后续如果有用到,可能会更新到博文。

<!-- Shiro -->
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
	<version>1.4.0</version>
</dependency>
<!-- Shiro-Thymeleaf -->
<dependency>
	<groupId>com.github.theborakompanioni</groupId>
	<artifactId>thymeleaf-extras-shiro</artifactId>
	<version>2.0.0</version>
</dependency>
<!-- Thymeleaf -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<!-- Redis缓存Shiro - 本文未使用,可不引入 -->
<dependency>
	<groupId>org.crazycake</groupId>
	<artifactId>shiro-redis</artifactId>
	<version>2.4.2.1-RELEASE</version>
</dependency>
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-ehcache</artifactId>
	<version>1.4.0</version>
</dependency>

二、基础准备

1、创建用户类(User)
@Data
public class User {
    /** 主键Id */
    private Long id;
    /** 账号 */
    private String username;
    /** 密码 */
    private String password;

    private List<Role> roles;
}
2、创建Service层(UserService)
@Service
public class UserServiceImpl implements UserService {

    @Override
    public User getUserByUsername(String username) {
        User user = new User();
        // 应该从数据库获取,这里写死了
        user.setId(1L);
        user.setUsername("lcy123456");
        user.setPassword("97fe5ea8b72e6a39bd9e500cb462e426");
        return user;
    }

    @Override
    public List<String> getRolesById(Long id) {
        List<String> roles = new ArrayList<>();
        // 应该从数据库获取,这里写死了
        roles.add("hr");
        roles.add("manager");
        return roles;
    }

    @Override
    public List<String> getPermissionById(Long id) {
        List<String> permissions = new ArrayList<>();
        // 应该从数据库获取,这里写死了
        permissions.add("role:index");
        permissions.add("menu:index");
        return permissions;
    }
}
3、静态登录页准备

登录页面
添加按钮在这里的意义是本来想演示:通过这个按钮发起请求,但是没有这个权限的解决办法。(实际没啥用,因为我这里没有权限是通过@ControllerAdvice做了统一处理),只需关注登录即可。

三、自定义AuthorizingRealm

创建一个类继承自AuthorizingRealm,这个类的作用就是用来认证与授权的。

public class UserRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    /**
     * 身份认证
     * 前端form表单通过post请求发送的/login请求
     * 会自动将name为username和password的值放到token里去
     * 当然也可以自己手动提交到token里去 - 本例的做法
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 从Token中获取账户
        String username = (String) authenticationToken.getPrincipal();
        // String password = new String((char[]) authenticationToken.getCredentials());  // 获取密码
        // 直接根据获取到的username去数据库登录用户对象
        User user = userService.getUserByUsername("lcy123456");
        if(user == null){
            throw new AccountException("用户名或密码错误!");
        }
        // 交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配
        // 参数:主体、正确的密码、盐、当前realm名称
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(
                user,
                user.getPassword(),
                ByteSource.Util.bytes(user.getUsername()),
                this.getName()
        );
        return info;
    }

    /**
     * 用户授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 获取用户身份信息
        User user = (User) principalCollection.getPrimaryPrincipal();
        // 根据当前角色去查询权限
        List<String> roles = userService.getRolesById(user.getId());
        List<String> permissions = userService.getPermissionById(user.getId());
        // 给授权信息
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addRoles(roles);
        info.addStringPermissions(permissions);
        return info;
    }
}

四、Shiro配置文件

@Configuration
public class ShiroConfig {

    /**
     * 配置ShiroDialect,用于Shiro和thymeleaf标签配合使用
     * 可以让Thymealf页面使用shiro标签
     * @return
     */
    @Bean
    public ShiroDialect shiroDialect(){
        return new ShiroDialect();
    }

    /**
     * 将自定义Realm交给Spring管理
     * @return UserRealm
     */
    @Bean
    public UserRealm userRealm(){
        UserRealm userRealm = new UserRealm();
        // 告诉Realm,使用credentialsMatcher加密算法类来验证密文
        userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        // 设置不允许缓存
        userRealm.setCachingEnabled(false);
        // userRealm.setAuthenticationCachingEnabled(true);   // 允许认证缓存
        // userRealm.setAuthenticationCacheName("authenticationCache");
        // userRealm.setAuthorizationCachingEnabled(true);    // 允许授权缓存
        // userRealm.setAuthorizationCacheName("authorizationCache");
        return userRealm;
    }

    /**
     * Shiro核心类:协调Shiro内部的各种安全组件
     * @return SecurityManager
     */
    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置realm
        securityManager.setRealm(userRealm());
//        // 自定义缓存实现 - 使用Redis
//        securityManager.setCacheManager(cacheManager());
//        // 自定义session管理 - 使用redis
//        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }

    
    /**
     * Shiro过滤器 - 访问权限控制
     * @param securityManager
     * @return
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 设置登录请求url  - 注销的url是/logout
        shiroFilterFactoryBean.setLoginUrl("/login");
//        shiroFilterFactoryBean.setSuccessUrl("/");  // 成功跳转地址
//        // 无权限跳转的请求 - 注解鉴权,这个不会生效
//        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

        /* *
         * 过滤链定义
         * 设置访问权限 - Map使用LinkedList,因为它是有顺序的
         * authc:需要验证的url   anno:无需验证的url
         */
        Map<String,String> filterChainMap = new LinkedHashMap<>();
        // 配置某个url需要某个权限码 - 通常用注解的方式
        // filterChainMap.put("/hello", "perms[how_are_you]");
        // 过滤掉静态文件css/js/images - Thymeleaf的静态文件一般放在resources/static下的
        filterChainMap.put("/css/**","anno");
        filterChainMap.put("/js/**","anno");
        filterChainMap.put("/images/**","anno");
        filterChainMap.put("/lib/**","anno");
        // 登录、注册、错误不需要验证
        filterChainMap.put("/login","anno");
        filterChainMap.put("/register","anno");
        filterChainMap.put("/error","anno");
        // 需要拦截验证的url
        filterChainMap.put("/admin/**","authc");
        filterChainMap.put("/user/**","authc");
        // 下面这行拦截所有代码必须在Map最后一个,否则会拦截所有url
        // 这样写的话,就是除了前面无需验证的,剩下的全都得验证
        filterChainMap.put("/**","authc");
        // 注意:认证成功/失败也可以通过shiro的过滤器来做 - FormAuthenticationFilter
        return shiroFilterFactoryBean;
    }

    /**
     * 凭证匹配器 - 匹配密码的规则
     * @return
     */
    @Bean(name = "credentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        // 使用MD5算法
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        // 散列此时 - 2次
        hashedCredentialsMatcher.setHashIterations(2);
        // 设置存储的凭证编码,默认为true:即Hex,如果为false,则为Base64编码
        hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return hashedCredentialsMatcher;
    }

    /************************************** 开启注解配置权限start ****************************************/
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 默认的代理创建者
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
     * @return
     */
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * 授权源
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }
    /************************************** 开启注解配置权限end ****************************************/


    /************************************** 开启Redis缓存start ****************************************/

//    /**
//     * cacheManager 缓存 redis实现
//     * 使用的是shiro-redis开源插件
//     * @return
//     */
//    public RedisCacheManager cacheManager() {
//        RedisCacheManager redisCacheManager = new RedisCacheManager();
//        redisCacheManager.setRedisManager(redisManager());
//        return redisCacheManager;
//    }
//    /**
//     * 配置shiro redisManager
//     * 使用的是shiro-redis开源插件
//     *
//     * @return
//     */
//    @Bean
//    public RedisManager redisManager() {
//        RedisManager redisManager = new RedisManager();
////        redisManager.setHost(host);
////        redisManager.setPort(port);
////        // 配置缓存过期时间
////        redisManager.setExpire(expireTime);
////        redisManager.setTimeout(timeOut);
////         redisManager.setPassword(password);
//        return redisManager;
//    }
//
//    /**
//     * Session Manager
//     * 使用的是shiro-redis开源插件
//     */
//    @Bean
//    public DefaultWebSessionManager sessionManager() {
//        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//        sessionManager.setSessionDAO(redisSessionDAO());
//        return sessionManager;
//    }
//
//    /**
//     * RedisSessionDAO shiro sessionDao层的实现 通过redis
//     * 使用的是shiro-redis开源插件
//     */
//    @Bean
//    public RedisSessionDAO redisSessionDAO() {
//        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
//        redisSessionDAO.setRedisManager(redisManager());
//        return redisSessionDAO;
//    }
    /************************************** 开启Redis缓存end ****************************************/
}

因为做了密码加密,所以注册的时候也需要对密码加密,如下所示:

@RunWith(SpringRunner.class)
@SpringBootTest
public class Test1 {
    @Test
    public void test(){
    	// MD5算法,对123456进行加盐并进行2次散列
        String md5Pwd = new SimpleHash("MD5", "123456",
                ByteSource.Util.bytes("lcy123456"), 2).toHex();
        System.out.println(md5Pwd);
    }
}

五、Thymeleaf页面使用Shiro标签

要想在thymeleaf使用shiro的标签,需要引入2.0以上的thymeleaf-extras-shiro依赖

<!DOCTYPE html>
<!-- 需加入对应的命名空间 -->
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<a href="/logout">注销</a>
<h1 shiro:hasPermission="role:index">role:index</h1>
<h1 shiro:hasPermission="role:indexAbc">role:indexAbc</h1>
<h1 shiro:hasRole="manager">manager</h1>
<h1 shiro:hasRole="abc">abc</h1>
</body>
</html>

六、Controller的书写

Controller层的书写主要是演示手动提交token和手动注销,以及权限注解RequiresPermissions的使用。

@Controller
public class ReController {
    /**
     * 返回JSON数据 - 跳转交给前端来做,这里只是示例
     * 但是似乎前端没有办法跳转到templates下的页面
     * 根据业务做,可以不返回Json数据,直接进行跳转即可
     * 这里因为前端使用的是axios/ajax请求,自己手动提交token
     * @param username 账号
     * @param password 密码
     * @return
     */
    @PostMapping("/login")
    @ResponseBody
    public String Login(@RequestParam("username") String username, @RequestParam("password") String password){
        // 从SecurityUtils里边创建一个 subject
        Subject subject = SecurityUtils.getSubject();
        // 在认证提交前准备 token(令牌)
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        // 执行认证登陆
        try {
            subject.login(token);
        } catch (UnknownAccountException uae) {
            return "未知账户";
        } catch (IncorrectCredentialsException ice) {
            return "密码不正确";
        } catch (LockedAccountException lae) {
            return "账户已锁定";
        } catch (ExcessiveAttemptsException eae) {
            return "用户名或密码错误次数过多";
        } catch (AuthenticationException ae) {
            return "用户名或密码不正确!";
        }
        if (subject.isAuthenticated()) {
            return "登录成功";
        } else {
            token.clear();
            return "登录失败";
        }
    }

    /**
     * 这个就是针对上面那个返回JSON的反面例子
     * 直接跳转到templates下的loginhtml.html
     * 这里只是给大家两种思路:可以直接返回页面,也可以让前端来跳转
     * @return
     */
    @GetMapping("/logout")
    public String logout(){
        Subject lvSubject=SecurityUtils.getSubject();
        // 注销
        lvSubject.logout();
        return "loginhtml";
    }

    /**
     * templates下的/loginhtml.html页面(登录页面)
     * @return
     */
    @GetMapping("/")
    public String index(){
        return "loginhtml";
    }


    @GetMapping("/index")
    public String indexTo(){
        return "index";
    }


    @GetMapping("/admin/add")
    @RequiresPermissions("user:list")
    public String add(){
        return "新增成功!";
    }

    @GetMapping("/role/index")
    @RequiresPermissions("role:index")
    public String addRole(){
        return "新增角色成功!";
    }

    @RequiresRoles("admin")
    @GetMapping("/admin")
    public String admin(){
    	return "角色admin可以访问!";
    ]
	@RequiresRoles(value = {"admin","user"},logical = Logical.OR)
    @GetMapping("/user")
    public String user(){
    	return "拥有admin或user角色可以访问!";
    }
}

七、无权限异常处理

@ControllerAdvice
public class ControllerExceptionHandler {
    @ExceptionHandler(AuthorizationException.class)
    public ModelAndView exceptionHandler(HttpServletRequest request, Exception e){
        ModelAndView mv = new ModelAndView();
        mv.setViewName("/403");
        return mv;
    }
}

以上代码总结:

基本演示了前后端分离和非前后端分离的解决方案(不全),对于无权访问也给出了解决方案,演示了Shiro标签在Thymealf页面上的使用。不足的是,因为没有具体的项目做支撑,因此很多地方想的不周全。这里推荐一篇博文:SpringBoot项目+Shiro(权限框架)+Redis(缓存)集成
最后就是:文中所有的return跳转页面,都是跳转的templates下的页面。而使用ajax请求的则是通过前端进行跳转的,则是在static下的页面。

补充:Shiro标签

Shiro标签原文

guest标签
  <shiro:guest>
  </shiro:guest>
  用户没有身份验证时显示相应信息,即游客访问信息。

user标签
  <shiro:user>  
  </shiro:user>
  用户已经身份验证/记住我登录后显示相应的信息。

authenticated标签
  <shiro:authenticated>  
  </shiro:authenticated>
  用户已经身份验证通过,即Subject.login登录成功,不是记住我登录的。

notAuthenticated标签
  <shiro:notAuthenticated>
  
  </shiro:notAuthenticated>
  用户已经身份验证通过,即没有调用Subject.login进行登录,包括记住我
    自动登录的也属于未进行身份验证。

principal标签
  <shiro: principal/>
  
  <shiro:principal property="username"/>
  相当于((User)Subject.getPrincipals()).getUsername()。

lacksPermission标签
  <shiro:lacksPermission name="org:create">
 
  </shiro:lacksPermission>
  如果当前Subject没有权限将显示body体内容。

hasRole标签
  <shiro:hasRole name="admin">  
  </shiro:hasRole>
  如果当前Subject有角色将显示body体内容。

hasAnyRoles标签
  <shiro:hasAnyRoles name="admin,user">
   
  </shiro:hasAnyRoles>
  如果当前Subject有任意一个角色(或的关系)将显示body体内容。

lacksRole标签
  <shiro:lacksRole name="abc">  
  </shiro:lacksRole>
  如果当前Subject没有角色将显示body体内容。

hasPermission标签
  <shiro:hasPermission name="user:create">  
  </shiro:hasPermission>
  如果当前Subject有权限将显示body体内容

补充 - 使用Shiro的Starter

<!-- 无需添加spring-boot-starter-web,已经依赖了 -->
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring-boot-web-starter</artifactId>
	<version>1.4.2</version>
</dependency>

用法和上面的基本一致,对于请求的过滤可以在ShiroConfig使用ShiroFilterChainDefinition代替ShiroFilterFactoryBean,示例:

@Bean 
public ShiroFilterChainDefinition shiroFilterChainDefinition(){
	DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
	// 除了这些,其余的全部都需要验证
	chainDefinition.addPathDefinition("/css/**","anno");
	chainDefinition.addPathDefinition("/js/**","anno");
	chainDefinition.addPathDefinition("/images/**","anno");
	chainDefinition.addPathDefinition("/lib/**","anno");
	chainDefinition.addPathDefinition("/login","anno");
	chainDefinition.addPathDefinition("/register","anno");
    return chainDefinition;
}

可在yml中配置Shiro

shiro:
  enabled: true        # 开启Shiro Web配置,默认为true
  loginUrl: /login     # 登录地址,默认为/login.jsp
  successUrl: /index   # 登录成功地址,默认为/
  unauthorizedUrl: /unauthorized    # 无权限跳转地址
  sessionManager:
    sessionIdCookieEnabled: true    # 是否允许通过URL参数实现会话跟踪,默认为true,如果网站支持Cookie,可以关闭选项
    sessionIdUrlRewritingEnabled: true  # 是否允许通过Cookie实现会话跟踪
发布了100 篇原创文章 · 获赞 25 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_40885085/article/details/105111404