全程配图超清晰的Springboot权限控制后台管理项目实战第二期(Springboot+shiro+mybatis+redis)

全程配图超清晰的Springboot权限控制后台管理项目实战第二期(Springboot+shiro+mybatis+redis)

众所周知,作为一个后端新手学习者,通过项目来学习,增长项目经验,是一个非常好的学习途径,所以我就找到了一个个人博客的项目,经过自己的调试和学习,现在已经比较完全的掌握了这个项目的创作过程,和一些细节方面的东西,在这里我把项目源码和项目实例都给出来,并且在后面进行一些细节和整体思路上的详解。

Springboot加Springsceurity实现权限管理系统

  • 全程配图超清晰的Springboot权限控制后台管理项目实战第二期(Springboot+shiro+mybatis+redis)
  • Springboot整合shiro
    • shiro
      • shiro整体思路
      • springboot整合mybatis链接数据库
    • jwt加密
      • jwt和MD5加密以及验证
  • shiro授权以及认证
    • shiro授权
    • shiro认证
    • AccountController(最终登录)
  • shiro总结

Springboot整合shiro

众所周知,shiro和springsecurity作为两个著名的框架,被人们广泛使用,当然这两个框架各有利弊,shiro是一个相对来说简单一点,好理解一点,所以大部分的项目都会使用这个框架进行安全认证,个人认为也是比较易上手的一个简单框架,但是SpringSecurity作为一个Spring家族的一员,是一个非常成熟而且功能强大的安全框架,在这里我们就来详细的介绍一下shiro的内容,源码也会在我的gitee仓库中,是一个比较好看的个人博客,欢迎大家来进行点评。

shiro

shiro整体思路

  1. shiro作为一个轻量级,易管理的安全框架,底层逻辑必然是学习必不可少的一个环节
    全程配图超清晰的Springboot权限控制后台管理项目实战第二期(Springboot+shiro+mybatis+redis)_第1张图片
  2. Subject
    主体,代表当前‘用户’ 。这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委派给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者。
  3. Principal
    身份信息,是主体(subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)
  4. Shiro SecurityManager
    安全管理器;即所有与安全有关的操作都会与SecurityManager交互且它管理者所有Subject;可以看出它是Shiro的核心,它负责与后面介绍的其它组件进行交互,可以把它看成DispathcherServlet前端控制器
  5. Realm
    数据域,Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源
  6. 专有名词
    全程配图超清晰的Springboot权限控制后台管理项目实战第二期(Springboot+shiro+mybatis+redis)_第2张图片

springboot整合mybatis链接数据库

  1. 导入mybatis的逆向工程,通过数据库的表数据来导入逆向工程生成entity,mapper,service,还有controller等类,这些类是不需要自己进行写的,毕竟是一些重复而没有太多技术要求的工作,因为一个项目可能会有很多的数据库,然后如果自己写的话,会花费很长时间,所以我们通过mybatis-plus的逆向工程来完成这一部分的内容。
package com.danli;

import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.FileType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中

public class CodeGenerator {

    /**
     * 

* 读取控制台内容 *

*/
public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("请输入" + tip + ":"); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotEmpty(ipt)) { return ipt; } } throw new MybatisPlusException("请输入正确的" + tip + "!"); } public static void main(String[] args) { // 代码生成器 AutoGenerator mpg = new AutoGenerator(); // 全局配置 GlobalConfig gc = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); gc.setOutputDir(projectPath + "/vueblog/src/main/java"); // gc.setOutputDir("D:\\test"); gc.setAuthor("fanfanli"); gc.setOpen(false); // gc.setSwagger2(true); 实体属性 Swagger2 注解 gc.setServiceName("%sService"); mpg.setGlobalConfig(gc); // 数据源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC"); // dsc.setSchemaName("public");b dsc.setDriverName("com.mysql.cj.jdbc.Driver"); dsc.setUsername("root"); dsc.setPassword("1234"); mpg.setDataSource(dsc); // 包配置 PackageConfig pc = new PackageConfig(); pc.setModuleName(null); pc.setParent("com.danli"); mpg.setPackageInfo(pc); // 自定义配置 InjectionConfig cfg = new InjectionConfig() { @Override public void initMap() { // to do nothing } }; cfg.setFileCreate((configBuilder, fileType, filePath) -> { //如果是Entity则直接返回true表示写文件 if (fileType == FileType.ENTITY) { return true; } //否则先判断文件是否存在 File file = new File(filePath); boolean exist = file.exists(); if (!exist) { file.getParentFile().mkdirs(); } //文件不存在或者全局配置的fileOverride为true才写文件 return !exist || configBuilder.getGlobalConfig().isFileOverride(); }); // 如果模板引擎是 freemarker String templatePath = "/templates/mapper.xml.ftl"; // 如果模板引擎是 velocity // String templatePath = "/templates/mapper.xml.vm"; // 自定义输出配置 List<FileOutConfig> focList = new ArrayList<>(); // 自定义配置会被优先输出 focList.add(new FileOutConfig(templatePath) { @Override public String outputFile(TableInfo tableInfo) { // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!! return projectPath + "/vueblog/src/main/resources/mapper/" + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML; } }); cfg.setFileOutConfigList(focList); mpg.setCfg(cfg); // 配置模板 TemplateConfig templateConfig = new TemplateConfig(); templateConfig.setXml(null); mpg.setTemplate(templateConfig); // 策略配置 StrategyConfig strategy = new StrategyConfig(); strategy.setNaming(NamingStrategy.underline_to_camel); strategy.setColumnNaming(NamingStrategy.underline_to_camel); strategy.setEntityLombokModel(true); strategy.setRestControllerStyle(true); strategy.setInclude(scanner("表名,多个英文逗号分割").split(",")); strategy.setControllerMappingHyphenStyle(true); strategy.setTablePrefix("m_"); mpg.setStrategy(strategy); mpg.setTemplateEngine(new FreemarkerTemplateEngine()); mpg.execute(); } }

sql文件也在我的项目中,在此不做过多叙述。

  1. 然后在UserMapper中编写sql语句,详情如下
package com.danli.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.danli.common.lang.vo.UserInfo;
import com.danli.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * 

* Mapper 接口 *

* * @author fanfanli * @date 2021-04-08 */
@Mapper @Repository public interface UserMapper extends BaseMapper<User> { /** * 获取用户部分信息list */ @Select("select id, nickname, username, avatar, email, status, create_time, update_time, role from user order by create_time desc") List<UserInfo> getUserInfo(); }
  1. 编写Userservice类以及Userserviceimpl实现类,具体实现代码如下

package com.danli.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.danli.common.lang.vo.UserInfo;
import com.danli.entity.User;

import java.util.List;

/**
 * user服务类
 *
 * @author fanfanli
 * @date  2021/4/5
 */
public interface UserService extends IService<User> {
    /**
     * 查询所有用户(只含有部分信息)
     *
     * @return 用户(只含有部分信息)list
     */
    List<UserInfo> getUserInfoList();

}



import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.danli.common.lang.vo.UserInfo;
import com.danli.entity.User;
import com.danli.mapper.UserMapper;
import com.danli.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * user服务实现类
 *
 * @author fanfanli
 * @date 2021-04-08
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Autowired
    UserMapper userMapper;

    /**
     * 查询所有用户(只含有部分信息)
     *
     * @return 用户(只含有部分信息)list
     */
    @Override
    public List<UserInfo> getUserInfoList(){
        List<UserInfo> userInfos = userMapper.getUserInfo();
        return  userInfos;
    }
}

至此,关于数据库链接的步骤就完成了,下面我们开始进行加密和授权以及最重要的认证

jwt加密

  1. 概念:数据加密的基本过程就是对原来为明文的文件或数据按照某种算法进行处理,使其成为不可读的一段代码,通常称为“密文”,使其只能在输入相应的密匙之后才能显示出本来内容,通过这样的途径来达到保护数据不被非法人窃取、阅读的目的。该过程的逆过程为解密,即将该编码信息转换为其原来数据的过程。

  2. 加密的分类
    (1)、对称加密
    双方使用的同一个密匙,既可以加密又可以解密,这种加密方法称为对称加密,也称单密匙加密。
    (2)、非对称加密
    一对密匙由公钥和私钥组成(可以使用很多对密匙)。私钥解密公钥加密数据,公钥解密私钥加密数据(私钥公钥可以互相加密解密)。

  3. 加密算法分类
    (1)、单项加密
    单项加密是不可逆的,也就是只能加密,不能解密。通常用来传输类型用户名和密码,直接将加密后的数据提交到后台,因为后台不需要知道用户名和密码,可以直接将接收到的加密后的数据存储到数据库
    (2)、双向加密
    通常分为对称性加密算法和非对称性加密算法,对于对称性加密算法,信息接收双方都需事先知道密匙和加解密算法且其密匙是相同的,之后便是对数据进行 加解密了。非对称算法与之不同,发送双方A,B事先均生成一堆密匙,然后A将自己的公有密匙发送给B,B将自己的公有密匙发送给A,如果A要给B发送消 息,则先需要用B的公有密匙进行消息加密,然后发送给B端,此时B端再用自己的私有密匙进行消息解密,B向A发送消息时为同样的道理。

jwt和MD5加密以及验证

  1. MD5的使用实例
    import org.apache.shiro.crypto.hash.Md5Hash;
    
    public class Md5HashTest {
    	public static void main(String[] args) {
    		// 对单个信息加密
    		Md5Hash md5 = new Md5Hash("123456");
    		System.out.println(md5.toString());
    		// 加密添加盐值 增大解密难度,让密码更咸一点
    		md5 = new Md5Hash("123456","aaa");
    		System.out.println(md5.toString());
    		// 加密添加盐值 增大解密难度  迭代1024次
    		md5 = new Md5Hash("123456789","aaa",1024);
    		System.out.println(md5);
    	}
    }
    
  2. jwt的思想介绍
    众所周知,jwt是所谓java web token的简称,也就是java web 身份令牌的简称,首先就是把jwt实现类实现出来,代码详情如下:
    package com.danli.util;
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import lombok.Data;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * jwt工具类
     */
    @Slf4j
    @Data
    @Component
    @ConfigurationProperties(prefix = "fanli.jwt")
    public class JwtUtils {
    
        private String secret;
        private long expire;
        private String header;
    
        /**
         * 生成 JWT Token 字符串
         *
         * @param userId       签发人id
         * expireDate       过期时间 签发时间
         * claims           额外添加到荷部分的信息。
         *                  例如可以添加用户名、用户ID、用户(加密前的)密码等信息
         */
        public String generateToken(long userId,String username) {
            Date nowDate = new Date();
            //过期时间
            Date expireDate = new Date(nowDate.getTime() + expire * 1000);
            Map<String, Object> claims = new HashMap<String, Object>();//创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
            claims.put("userId", userId+"");
            claims.put("username", username);
            return Jwts.builder()    // 创建 JWT 对象
                    .setHeaderParam("typ", "JWT")  //设置头部信息
                    .setClaims(claims)     // 设置私有声明
                    .setIssuedAt(nowDate)        //设置payload的签发时间
                    .setExpiration(expireDate)   //设置payload的过期时间
                    .signWith(SignatureAlgorithm.HS512, secret)  // 设置安全密钥(生成签名所需的密钥和算法)
                    .compact();            //生成token(1.编码 Header 和 Payload 2.生成签名 3.拼接字符串)
        }
        /**
         *  解析token
         * JWT Token 由 头部 荷载部 和 签名部 三部分组成。签名部分是由加密算法生成,无法反向解密。
         * 而 头部 和 荷载部分是由 Base64 编码算法生成,是可以反向反编码回原样的。
         * 这也是为什么不要在 JWT Token 中放敏感数据的原因。
         *
         * @param token 加密后的token
         * @return claims 返回荷载部分的键值对
         */
        public Claims getClaimByToken(String token) {
            try {
                return Jwts.parser()           // 创建解析对象
                        .setSigningKey(secret)   // 设置安全密钥(生成签名所需的密钥和算法)
                        .parseClaimsJws(token)  // 解析token
                        .getBody();     // 获取 payload 部分内容
            } catch (Exception e) {
                log.debug("validate is token error ", e);
                return null;
            }
        }
    
        /**
         * token是否过期
         *
         * @return true:过期
         */
        public boolean isTokenExpired(Date expiration) {
            return expiration.before(new Date());
        }
    }
    
  3. Token的实体类
    这个实体类就类似数据库表的这种实体类,但是作用还是进行数据传输。
    package com.danli.shiro;
    
    import org.apache.shiro.authc.AuthenticationToken;
    
    /**
     * Jwt
     *
     * @author fanfanli
     * @date  2021/5/28
     */
    public class JwtToken implements AuthenticationToken {
        private String token;
    
        public JwtToken(String token) {
            this.token = token;
        }
    
        @Override
        public Object getPrincipal() {
            return token;
        }
    
        @Override
        public Object getCredentials() {
            return token;
        }
    }
    
  4. 编写shiro过滤器
    从我们一开始学习Servlet,我们就开始接触一个东西叫做过滤器,那么我们的shiro配置里面也需要用到一个过滤器,这个过滤器的作用就是通过token的验证,例如验证token是否存在,然后验证token是否过期以及最后的验证登录是否异常,当然还有尊带解决一下跨域问题代码详情如下:
    package com.danli.shiro;
    
    import cn.hutool.json.JSONUtil;
    import com.danli.common.lang.Result;
    import com.danli.util.JwtUtils;
    import io.jsonwebtoken.Claims;
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.authc.ExpiredCredentialsException;
    import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
    import org.apache.shiro.web.util.WebUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    /**
     * shiro过滤器
     *
     * @author fanfanli
     * @date  2021/5/28
     */
    @Component
    public class JwtFilter extends AuthenticatingFilter {
    
        @Autowired
        JwtUtils jwtUtils;
    
        @Override
        protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
            // 获取 token
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            String jwt = request.getHeader("Authorization");
            if (!StringUtils.hasLength(jwt)) {
                return null;
            }
            return new JwtToken(jwt);
        }
    
        @Override
        protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            String token = request.getHeader("Authorization");
            if (!StringUtils.hasLength(token)) {
                return true;
            } else {
                // 判断是否已过期
                Claims claim = jwtUtils.getClaimByToken(token);
                if (claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
                    throw new ExpiredCredentialsException("token已失效,请重新登录!");
                }
            }
            // 执行自动登录
            return executeLogin(servletRequest, servletResponse);
        }
    
        @Override
        protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            try {
                //处理登录失败的异常
                Throwable throwable = e.getCause() == null ? e : e.getCause();
                Result r = Result.fail(throwable.getMessage());
                String json = JSONUtil.toJsonStr(r);
                httpResponse.getWriter().print(json);
            } catch (IOException e1) {
            }
            return false;
        }
    
        /**
         * 对跨域提供支持
         */
        @Override
        protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
            HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
            HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
            httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
            httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
            httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
            // 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
            if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
                httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
                return false;
            }
            return super.preHandle(request, response);
        }
    }
    
  5. 只有这些还不够,需要一个config类来整体调整shiro的整体配置,代码详情如下:
    package com.danli.config;
    
    import com.danli.shiro.AccountRealm;
    import com.danli.shiro.JwtFilter;
    import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
    import org.apache.shiro.mgt.DefaultSubjectDAO;
    import org.apache.shiro.mgt.SecurityManager;
    import org.apache.shiro.session.mgt.SessionManager;
    import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
    import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
    import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
    import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
    import org.crazycake.shiro.RedisCacheManager;
    import org.crazycake.shiro.RedisSessionDAO;
    import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.servlet.Filter;
    import java.util.HashMap;
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    /**
     * shiro配置类
     *
     * @author fanfanli
     * @date  2021/4/8
     */
    @Configuration
    public class ShiroConfig {
    
        @Autowired
        JwtFilter jwtFilter;
    
        @Bean
        public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
            DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    
            // inject redisSessionDAO
            sessionManager.setSessionDAO(redisSessionDAO);
    
            return sessionManager;
        }
    
        @Bean
        public DefaultWebSecurityManager securityManager(AccountRealm accountRealm,
                                                         SessionManager sessionManager,
                                                         RedisCacheManager redisCacheManager) {
    
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);
    
            //inject sessionManager
            securityManager.setSessionManager(sessionManager);
    
    
            //关闭shiro自带的session
            DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
            DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
            defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
            subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
            securityManager.setSubjectDAO(subjectDAO);
    
    
            // inject redisCacheManager
            securityManager.setCacheManager(redisCacheManager);
            return securityManager;
        }
    
        @Bean
        public ShiroFilterChainDefinition shiroFilterChainDefinition() {
            DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
    
            Map<String, String> filterMap = new LinkedHashMap<>();
    
            filterMap.put("/**", "jwt");
            chainDefinition.addPathDefinitions(filterMap);
            return chainDefinition;
        }
    
        @Bean("shiroFilterFactoryBean")
        public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
                                                             ShiroFilterChainDefinition shiroFilterChainDefinition) {
            ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
            shiroFilter.setSecurityManager(securityManager);
    
            Map<String, Filter> filters = new HashMap<>();
            //使用jwtFilter过滤器
            filters.put("jwt", jwtFilter);
            shiroFilter.setFilters(filters);
    
            Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
    
            shiroFilter.setFilterChainDefinitionMap(filterMap);
            return shiroFilter;
        }
    
        /**
         * 解决aop与shiro冲突问题
         */
        @Bean
        public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
    
            DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
            defaultAdvisorAutoProxyCreator.setUsePrefix(true);
    
            return defaultAdvisorAutoProxyCreator;
        }
    }
    
  6. 现在整体的shiro配置已经完成,然后我们就会通过自定义的realm进行数据的认证和授权,可以说这个realm是shiro最重要的一个类,这个类中不仅进行数据认证,当然还有数据授权。在这里我们还要进行一个实体类的编写,方便我们接下来自定义realm类进行数据验证,也是类似上面的JwtToken,是一个实体类,详情代码如下。
    package com.danli.shiro;
    
    import lombok.Data;
    
    import java.io.Serializable;
    
    /**
     * 账户信息实体类,用来介绍账户信息
     *
     * @author fanfanli
     * @date  2021/5/28
     */
    @Data
    public class AccountProfile implements Serializable {
        private Long id;
        private String username;
        private String avatar;
        private String role;//角色
    }
    
  7. 那么现在我们的脚手架配置以及基本实体类都已经配置和编写完成,下面就开始实现一下详细代码,进行数据授权和认证。

shiro授权以及认证

  1. 好的现在我们进入到真正的验证逻辑里面,先吧AccountRealm这个自定义Realm类的详情实现出来,下面是代码实例
    package com.danli.shiro;
    
    import cn.hutool.core.bean.BeanUtil;
    import com.danli.entity.User;
    import com.danli.service.UserService;
    import com.danli.util.JwtUtils;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.shiro.authc.*;
    import org.apache.shiro.authz.AuthorizationInfo;
    import org.apache.shiro.authz.SimpleAuthorizationInfo;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    /**
     * 登录认证和授权
     *
     * @author fanfanli
     * @date  2021/5/28
     */
    @Slf4j
    @Component
    public class AccountRealm extends AuthorizingRealm {
        @Autowired
        JwtUtils jwtUtils;
        @Autowired
        UserService userService;
    
        @Override
        public boolean supports(AuthenticationToken token) {
            return token instanceof JwtToken;
        }
        /**
         * 授权
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            log.info("执行doGetAuthorizationInfo方法进行授权");
    //        String username = JwtUtil.getUsername(principalCollection.toString());
            log.info("登录的用户:"+principals.toString());
    //        log.info("登录的用户:" + username);
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    
            AccountProfile accountProfile = (AccountProfile)principals.getPrimaryPrincipal();
            String[] roles = accountProfile.getRole().split(",");
            log.info("roles");
            for(String role : roles){
                info.addRole(role);
                if(role.equals("role_root")){
                    info.addStringPermission("user:create");
                    info.addStringPermission("user:update");
                    info.addStringPermission("user:read");
                    info.addStringPermission("user:delete");
                }
                else if( role.equals("role_admin")){
                    info.addStringPermission("user:read");
                    info.addStringPermission("user:create");
                    info.addStringPermission("user:update");
                }
                else if( role.equals("role_user")){
                    info.addStringPermission("user:read");
                    info.addStringPermission("user:create");
                }
                else if(role.equals("role_guest")){
                    info.addStringPermission("user:read");
                }
            }
    
    
            return info;
    
        }
    
        /**
         * 认证
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            JwtToken jwt = (JwtToken) token;
            log.info("jwt----------------->{}", jwt);
            String userId = (String) jwtUtils.getClaimByToken((String) jwt.getPrincipal()).get("userId");
            String username = (String) jwtUtils.getClaimByToken((String) jwt.getPrincipal()).get("username");
            User user = userService.getById(Long.parseLong(userId));
            if (user == null) {
                throw new UnknownAccountException("账户不存在!");
            }
            if (user.getStatus() == -1) {
                throw new LockedAccountException("账户已被锁定!");
            }
            if(!user.getUsername().equals(username)){
                throw new UnknownAccountException("userId与username不一致");
            }
            AccountProfile profile = new AccountProfile();
            //知道它的身份 principals
            BeanUtil.copyProperties(user, profile);
            log.info("profile----------------->{}", profile.toString());
            return new SimpleAuthenticationInfo(profile, jwt.getCredentials(), getName());
        }
    }
    
    可能代码量有一些大,下面我们来进行详情的介绍。
  2. 这个Accountrealm实现了一个AuthorizingRealm的类,这个类中有两个抽象方法,是我们需要单独实现的,分别是完成用户认证的doGetAuthentictionInfo方法,再有就是完成用户授权的doGetAuthorizationInfo方法,这两个方法是不是特别像我们一开始学习servclet的时候,实现的那两个方法,一个是doGet,一个是doPost方法。

shiro授权

  1. 什么是授权
    授权可简单理解为who对what(which)进行How操作:

    Who,即主体(Subject),主体需要访问系统中的资源。

    What,即资源(Resource),如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型和资源实例,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例。

    How,权限/许可(Permission),规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。

  2. 授权方式

    1. 基于角色的访问控制,RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制

      if(subject.hasRole("admin")){
         //操作什么资源
      }
      
      
    2. 基于资源的访问控制
      RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制

      if(subject.isPermission("user:update:01")){ //资源实例
        //对01用户进行修改
      }
      if(subject.isPermission("user:update:*")){  //资源类型
        //对01用户进行修改
      }
      
  3. 权限字符串

    ​ 权限字符串的规则是:资源标识符:操作:资源实例标识符。
    意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,权限字符串也可以使用*通配符。

    例子:

     用户创建权限:user:create,或user:create:*
     用户修改实例001的权限:user:update:001
     用户实例001的所有权限:user:*:001
    

    权限的编码方式

    1. 编程式
      Subject subject = SecurityUtils.getSubject();
      if(subject.hasRole("admin")) {
      	//有权限
      } else {
      	//无权限
      }
      
    2. 注解式
      @RequiresRoles("admin")
      public void hello() {
      	//有权限
      }
      
  4. 分析代码实例:这里通过principals参数的传递,进行数据的传输,然后从参数中提取出用户的角色,通过这个角色来判断权限,并且赋予他相对应的权限,权限赋予之后,我们就返回这个数组,这个数组就成为了这个用户的功能权限数组,在后面验证就能直接进行判断。因为这个个人博客只有简单的修改,删除,添加,查看几个操作,在这里也就不再去多设置别的数据库来专门存储这个权限,当然如果是功能非常多,就最好单独设置这个数据库,然后通过多表查询,来进行权限授权,这个方法过段时间我会通过SpringSecurity来详细的讲一下,敬请期待。

    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("执行doGetAuthorizationInfo方法进行授权");
        log.info("登录的用户:"+principals.toString());
    
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    
        AccountProfile accountProfile = (AccountProfile)principals.getPrimaryPrincipal();
        String[] roles = accountProfile.getRole().split(",");
        log.info("roles");
        for(String role : roles){
            info.addRole(role);
            if(role.equals("role_root")){
                info.addStringPermission("user:create");
                info.addStringPermission("user:update");
                info.addStringPermission("user:read");
                info.addStringPermission("user:delete");
            }
            else if( role.equals("role_admin")){
                info.addStringPermission("user:read");
                info.addStringPermission("user:create");
                info.addStringPermission("user:update");
            }
            else if( role.equals("role_user")){
                info.addStringPermission("user:read");
                info.addStringPermission("user:create");
            }
            else if(role.equals("role_guest")){
                info.addStringPermission("user:read");
            }
        }
    
    
        return info;
    
    }
    

shiro认证

  1. 身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确,
  2. 代码详情:
    /**
     * 认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        JwtToken jwt = (JwtToken) token;
        log.info("jwt----------------->{}", jwt);
        String userId = (String) jwtUtils.getClaimByToken((String) jwt.getPrincipal()).get("userId");
        String username = (String) jwtUtils.getClaimByToken((String) jwt.getPrincipal()).get("username");
        User user = userService.getById(Long.parseLong(userId));
        if (user == null) {
            throw new UnknownAccountException("账户不存在!");
        }
        if (user.getStatus() == -1) {
            throw new LockedAccountException("账户已被锁定!");
        }
        if(!user.getUsername().equals(username)){
            throw new UnknownAccountException("userId与username不一致");
        }
        AccountProfile profile = new AccountProfile();
        //知道它的身份 principals
        BeanUtil.copyProperties(user, profile);
        log.info("profile----------------->{}", profile.toString());
        return new SimpleAuthenticationInfo(profile, jwt.getCredentials(), getName());
    }
    
  3. 可以看到这里的代码就是把jwt获取出来,然后提取username和password并对比他们是不是都一样,然后在控制台输出身份信息,一旦有问题就直接抛出异常,如果都没错,就返回信息,通过用户认证。

AccountController(最终登录)

  1. 那么现在我们就把shiro的脚手架,以及项目的基本结构都实现完成了,紧接着我们就需要编写真正的登录接口,来实现单点登录功能,具体代码如下:
    package com.danli.controller;
    import cn.hutool.core.map.MapUtil;
    import cn.hutool.crypto.SecureUtil;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.danli.common.lang.Result;
    import com.danli.common.lang.dto.LoginDto;
    import com.danli.entity.User;
    import com.danli.service.UserService;
    import com.danli.util.JwtUtils;
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authz.annotation.RequiresAuthentication;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.util.Assert;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.*;
    import javax.servlet.http.HttpServletResponse;
    /**
     * 登录登出控制器
     *
     * @author fanfanli
     * @date  2021/4/8
     */
    @RestController
    public class AccountController {
        @Autowired
        JwtUtils jwtUtils;
        @Autowired
        UserService userService;
        /**
         * 登录请求处理
         */
        @CrossOrigin
        @PostMapping("/login")
        public Result login(@Validated @RequestBody LoginDto loginDto, HttpServletResponse response) {
            User user = userService.getOne(new QueryWrapper<User>().eq("username", loginDto.getUsername()));
            Assert.notNull(user, "用户名或密码错误");
            if (!user.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))) {
                return Result.fail("用户名或密码错误!");
            }
            if(user.getStatus()==0){
                return Result.fail("账户已被禁用");
            }
            String jwt = jwtUtils.generateToken(user.getId(),user.getUsername());
            response.setHeader("Authorization", jwt);
            response.setHeader("Access-Control-Expose-Headers", "Authorization");
            // 用户可以另一个接口
            return Result.succ(MapUtil.builder()
                    .put("id", user.getId())
                    .put("username", user.getUsername())
                    .put("avatar", user.getAvatar())
                    .put("email", user.getEmail())
                    .put("role", user.getRole())
                    .map()
            );
        }
        /**
         * 登出请求处理
         */
        @GetMapping("/logout")
        @RequiresAuthentication
        public Result logout() {
            SecurityUtils.getSubject().logout();
            return Result.succ("退出成功");
        }
    }
    
  2. 当然别忘了在UserService 中添加相应的接口,并且在UserServiceipml中实现相应的数据库查询操作,并且在userMapper中实现查询,在这里我们需要单独实现mapper的方法,因为我们这是查询一部分内容,所以不能使用mybatis-plus的默认方法。详细代码如下:
    	package com.danli.mapper;
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.danli.common.lang.vo.UserInfo;
    import com.danli.entity.User;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Select;
    import org.springframework.stereotype.Repository;
    
    import java.util.List;
    
    /**
     * 

    * Mapper 接口 *

    * * @author fanfanli * @date 2021-04-08 */
    @Mapper @Repository public interface UserMapper extends BaseMapper<User> { /** * 获取用户部分信息list */ @Select("select id, nickname, username, avatar, email, status, create_time, update_time, role from user order by create_time desc") List<UserInfo> getUserInfo(); } package com.danli.service; import com.baomidou.mybatisplus.extension.service.IService; import com.danli.common.lang.vo.UserInfo; import com.danli.entity.User; import java.util.List; /** * 服务类 * * @author fanfanli * @date 2021/4/5 */ public interface UserService extends IService<User> { /** * 查询所有用户(只含有部分信息) * * @return 用户(只含有部分信息)list */ List<UserInfo> getUserInfoList(); } package com.danli.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.danli.common.lang.vo.UserInfo; import com.danli.entity.User; import com.danli.mapper.UserMapper; import com.danli.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** * 服务实现类 * * @author fanfanli * @date 2021-04-08 */ @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Autowired UserMapper userMapper; /** * 查询所有用户(只含有部分信息) * * @return 用户(只含有部分信息)list */ @Override public List<UserInfo> getUserInfoList(){ List<UserInfo> userInfos = userMapper.getUserInfo(); return userInfos; } }
  3. 现在我们就能实现登录和权限认证的功能了

shiro总结

  1. 至此,我们的shiro安全认证也就完成了,我们首先编写了一些比较有用的工具类,比如jwt工具类,然后配置了jwtfilter过滤器,紧接着我们编写配置类ShiroConfiger来使用这个过滤器,同时还要顺带配置了一下跨域的问题。最重要的莫过于这个Realm数据认证和用户授权的类,直接解决了我们最重要的登录认证和用户授权的问题,最后我们通过编写AccountController类来实现了用户单点登录的功能。

你可能感兴趣的:(redis,spring,boot,java)