springboot+shiro前后端分离实现!!_spring shiro 前后端分离_码上编程的博客-程序员秘密

技术标签: spring boot  java  shiro  后端java  

本文章参考两位大佬写的博客完成的

1、前言

1.1、唠嗑部分

学习shiro之前,建议先学习springsecurity,将两个框架进行对比的方式学习,会有更加深的印象。我之前写过一篇关于springsecurity的博客,你可以参考参考,个人感觉挺详细的。超全的springboot+springsecurity前后端分离简单实现!!!_码上编程的博客-程序员秘密

本篇文章是使用shiro做认证授权的,基于token的前后端分离的小demo,而token我把它放在redis缓存里面,极大程度地模拟实战效果,本篇文章每一步都有详细步骤,看不懂可以看多几遍!!! 源代码在此文章最底部!!!

1.2、目标效果:

未登录情况下访问目标资源, 将会提示需要登录的字样。 /hello和/index是我在controller实现的简单接口。

 账号错误,并返回登录失败的字样

密码错误,并返回登录失败的字样 

 登录并返回登录成功字样以及token

登录成功的情况下访问拥有指定权限的目标资源,并返回该目标资源

 登录成功的情况下访问不具有指定权限的目标资源,并返回权限不足的提示字样

注销,并返回注销成功字样,同时删除缓存里面的token值

1.3、技术支持:

springbootshiro、mybatis-plus、mysql、redis、gson、lombok

2、核心部分 

2.1、原理

简单理解: shiro最重要的三个部分:

  • subject(当前用户)
  • securityManager (管理所有用户)
  • realm(数据交互)

复杂理解就在流程图里面:  强烈建议搞清楚流程图!!!!!

2.2、采坑部分

  • 不走doGetAuthenticationInfo(AuthenticationToken token) 方法, 通过在pom文件添加这样的依赖即可解决此问题!  <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
  • 拦截所有路径位置要放正确,要不然它会拦截所有路径,即 filterMap.put("/**","auth"); 要放在最后面

filterMap.put("/login","anon");  //放行login接口
       filterMap.put("/logout","anon");    //放行logout接口
       filterMap.put("/**","auth");    //拦截所有路径

2.3、代码

pom文件

<dependencies>
        <!--解决doGetAuthorizationInfo不生效问题-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- 配置使用redis启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--gson可以将对象转成json字符串-->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--集成jwt实现token认证-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.2.0</version>
        </dependency>
        <!--lombok依赖-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--mybatis-plus依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.1</version>
        </dependency>
        <!--模板引擎-->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.2</version>
        </dependency>
         <!--自动生成代码依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.4.1</version>
            <scope>test</scope>
        </dependency>
        <!--整合Shiro安全框架-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
       

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

数据库设计 ,这里我偷懒了,用的还是springsecurity那个例子的数据库

User.java

Data
@EqualsAndHashCode(callSuper = false)
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    private String account;

    private String password;

    private String role;


}

UserMapper.java继承BaseMapper<User>,BaseMapper<User>是mybatis-plus封装好大量基本sql的一个类,直接调用指定代码即可,不用手写sql,加快开发速度。

@Repository
public interface UserMapper extends BaseMapper<User> {

}

UserService.java

public interface UserService extends IService<User> {
    //根据账号查找用户
    User findByUsername(String username);
}

UserServiceImpl.java

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Autowired
    UserMapper userMapper;

    @Override
    public User findByUsername(String username) {
        //相当于select * from user where account='${username}'
        QueryWrapper<User> wrapper=new QueryWrapper<>();
        wrapper.eq("account",username);
        //user即为查询结果
        return userMapper.selectOne(wrapper);
    }
}

Msg.java,封装了返回的结果集,这个看个人的,怎么开心怎么来!

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Msg {
    int code;   //错误码
    String Message; //消息提示
    Map<String,Object> data=new HashMap<String,Object>();   //数据

    //无权访问
    public static Msg denyAccess(String message){
        Msg result=new Msg();
        result.setCode(300);
        result.setMessage(message);
        return result;
    }

    //操作成功
    public static Msg success(String message){
        Msg result=new Msg();
        result.setCode(200);
        result.setMessage(message);
        return result;
    }

    //客户端操作失败
    public static Msg fail(String message){
        Msg result=new Msg();
        result.setCode(400);
        result.setMessage(message);
        return result;
    }

    public Msg add(String key,Object value){
        this.data.put(key,value);
        return this;
    }
}

ShiroConfig.java,详细请看注释

@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();

        //关联 DefaultWebSecurityManager
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        //添加shiro内置的过滤器
        /*
        * anon: 无需认证就可以访问
        * authc: 必须认证才能访问
        * user: 必须拥有记住我功能才能用
        * perms: 拥有对某个资源的权限才能访问
        * role: 拥有某个角色权限才能访问
        * */
        //添加过滤器
        Map<String,Filter> filters=new HashMap<>();
        filters.put("auth",new AuthFilter());        //自定义的认证授权过滤器
        shiroFilterFactoryBean.setFilters(filters);  //添加自定义的认证授权过滤器

        //要拦截的路径放在map里面
        Map<String,String> filterMap=new LinkedHashMap<String,String>();
        filterMap.put("/login","anon");  //放行login接口
        filterMap.put("/logout","anon");    //放行logout接口
        filterMap.put("/**","auth");    //拦截所有路径, 它自动会跑到 AuthFilter这个自定义的过滤器里面
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        return shiroFilterFactoryBean;
    }

    //配置securityManager的实现类,变向的配置了securityManager
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(AuthRealm authRealm){
        DefaultWebSecurityManager defaultWebSecurityManager=new DefaultWebSecurityManager();

        //关联realm
        defaultWebSecurityManager.setRealm(authRealm);

        /*
         * 关闭shiro自带的session
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        defaultWebSecurityManager.setSubjectDAO(subjectDAO);
        return defaultWebSecurityManager;
    }

    //将自定义realm注入到 DefaultWebSecurityManager
    @Bean
    public AuthRealm authRealm(){
        return new AuthRealm();
    }


    //通过调用Initializable.init()和Destroyable.destroy()方法,从而去管理shiro bean生命周期
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    //开启shiro权限注解
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager defaultWebSecurityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(defaultWebSecurityManager);
        return advisor;
    }
}

自定义认证授权过滤器  AuthFilter ,  正常来说对于常用部分需要封装,这样代码冗余度就不会那么大,也比较优雅,但是这只是个小demo,不想把它弄的那么复杂、不利于理解,索性就不封装了。

1、用户发起请求首先进入isAccessAllowed()方法,拦截除了option请求 以外的请求, 浏览器机制就是:在发post、get请求之前,首先会发个option请求进行试探,所以要放行option请求通过,否则你的get、post请求永远进不来。

2、token不为空的情况下则生成属于自己的token,即创建一个类使其继承  UsernamePasswordToken此类

3、然后进入onAccessDenied()方法去校验token是否存在,并执行executeLogin(request,response)方法

public class AuthFilter extends AuthenticatingFilter {

    Gson gson=new Gson();

    //生成自定义token
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest= (HttpServletRequest) request;
        //从header中获取token
        String token=httpServletRequest.getHeader("token");
        return new AuthToken(token);
    }

    //所有请求全部拒绝访问
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //允许option请求通过
        if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
            return true;
        }
        return false;
    }

    //拒绝访问的请求,onAccessDenied方法先获取 token,再调用executeLogin方法
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest= (HttpServletRequest) request;
        HttpServletResponse httpServletResponse= (HttpServletResponse) response;
        String token=httpServletRequest.getHeader("token");     //获取请求token

        //StringUtils.isBlank(String str)  判断str字符串是否为空或者长度是否为0
        if(org.apache.commons.lang3.StringUtils.isBlank(token)){
            httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpServletResponse.setHeader("Access-Control-Allow-Origin",httpServletRequest.getHeader("Origin") );
            httpServletResponse.setCharacterEncoding("UTF-8");
            Msg msg= Msg.fail("请先登录");
            httpServletResponse.getWriter().write(gson.toJson(msg));
            return false;
        }
       return executeLogin(request,response);
    }

    //token失效时调用
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest= (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setContentType("application/json;charset=utf-8");
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpResponse.setCharacterEncoding("UTF-8");
        try {
            //处理登录失败的异常
            Throwable throwable = e.getCause() == null ? e : e.getCause();
            Msg msg=Msg.fail("登录凭证已失效,请重新登录");
            httpResponse.getWriter().write(gson.toJson(msg));
        } catch (IOException e1) {
        }
        return false;
    }
}

自定义 AuthToken.java 去继承 UsernamePasswordToken这个类,因为源码里的subject.login(token)需要传入一个自定义的token参数

public class AuthToken extends UsernamePasswordToken{
    String token;

    public AuthToken(String token){
        this.token=token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

AuthRealm.java是自定义的realm,继承AuthorizingRealm,实现doGetAuthenticationInfo(AuthenticationToken token)认证doGetAuthorizationInfo(PrincipalCollection principals)授权 

public class AuthRealm extends AuthorizingRealm {

    @Autowired
    UserServiceImpl userServiceImpl;

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //获取前端传来的token
        String accessToken= (String) token.getPrincipal();

        //redis缓存中这样存值, key为token,value为username
        //根据token去缓存里查找用户名
        String username=stringRedisTemplate.opsForValue().get(accessToken);
        if(username==null){
            //查找的用户名为空,即为token失效
            throw new IncorrectCredentialsException("token失效,请重新登录");
        }

        User user = userServiceImpl.findByUsername(username);
        if(user==null){
            throw new UnknownAccountException("用户不存在!");
        }

        //此方法需要返回一个AuthenticationInfo类型的数据
        // 因此返回一个它的实现类SimpleAuthenticationInfo,将user以及获取到的token传入它可以实现自动认证
        SimpleAuthenticationInfo simpleAuthenticationInfo=new SimpleAuthenticationInfo(user,accessToken,"");
        return simpleAuthenticationInfo;
    }

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //从认证那里获取到用户对象User
        User user = (User) principals.getPrimaryPrincipal();
        
        //此方法需要一个AuthorizationInfo类型的返回值,因此返回一个它的实现类SimpleAuthorizationInfo 
        //通过SimpleAuthorizationInfo里的addStringPermission()设置用户的权限
        SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addStringPermission(user.getRole());
        return simpleAuthorizationInfo;
    }

自定义异常处理器  MyExceptionHandler.java

//@ControllerAdvice可以实现全局异常处理,可以简单理解为增强了的controller
@ControllerAdvice
public class MyExceptionHandler {

    //捕获AuthorizationException的异常
    @ExceptionHandler(value = AuthorizationException.class)
    @ResponseBody
    public Msg handleException(AuthorizationException e) {
        Msg msg=Msg.denyAccess("权限不足呀!!!!!");
        return msg;
    }
}

UserController.java

@RestController
public class UserController {
    @Autowired
    UserServiceImpl userServiceImpl;

    //通过java去操作redis缓存string类型的数据
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    //需要权限为ROLE_USER才能访问/index
    @RequiresPermissions("ROLE_USER")
    @GetMapping("/index")
    public Msg index(@RequestHeader String token){
        return Msg.success("index");
    }

    //需要权限ROLE_ADMIN才能访问hello
    @RequiresPermissions("ROLE_ADMIN")
    @GetMapping("/hello")
    public Msg hello(@RequestHeader String token){
        return Msg.success("hello");
    }

    //登录接口
    @PostMapping("/login")
    public Msg login(@RequestParam("username")String username,@RequestParam("password")String password){
        User user = userServiceImpl.findByUsername(username);
        Msg msg=null;
        if (user == null) {
            msg = Msg.fail("账号错误");
        } else if (!password.equals(user.getPassword())) {
            msg = Msg.fail("密码错误");
        } else {
            //通过UUID生成token字符串,并将其以string类型的数据保存在redis缓存中,key为token,value为username
            String token= UUID.randomUUID().toString().replaceAll("-","");
            stringRedisTemplate.opsForValue().set(token,username,3600,TimeUnit.SECONDS);
            msg=Msg.success("登录成功").add("token",token);
        }
        return msg;
    }

    //注销接口
    @PostMapping("/logout")
    public Msg logout(@RequestHeader("token")String token){
        //删除redis缓存中的token
        stringRedisTemplate.delete(token);
        return Msg.success("注销成功");
    }

}

application.yml配置文件

server:
  port: 80

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/springsecurity_test?characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

  redis:
    host: localhost
    port: 6379

源代码在https://gitee.com/liu-wenxin/shiro_token_demo.git,通过git clone https://gitee.com/liu-wenxin/shiro_token_demo.git 下载使用

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_42375707/article/details/111145907

智能推荐

windows终端执行python文件函数_电脑控制台如何执行python main.py_向前看sf→→→的博客-程序员秘密

以前在cmd终端执行XX.py文件可以直接用python XX.py ,如下图:但是如果写的文件里面有函数,我要导入这个函数就不好弄了,下面是我执行文件函数的步骤:1.按windows+R2.输入cmd,打开终端3,找到要执行文件的地址,(我的文件在)所以我就一步步转到kNN.py的目录下4.这个时候也就是最重要的一步,在knn.py目录下输入python,...

SQL Profiler工具简介_baobaojinjin的博客-程序员秘密

一、SQL Profiler工具简介SQL Profiler是一个图形界面和一组系统存储过程,其作用如下:图形化监视SQL Server查询;在后台收集查询信息;分析性能;诊断像死锁之类的问题;调试T-SQL语句;模拟重放SQL Server活动;也可以使用SQL Profiler捕捉在SQL Server实例上执行的活动。这样的活动被称为Profiler跟踪...

JAVA程序员面试32问_创建网页nx.jsp,在其中声明函数nx(int x),对正整数x逆序输出。_Li_soso的博客-程序员秘密

JAVA程序员面试32问 第一,谈谈final, finally, finalize的区别。final-修饰符(关键字)如果一个类被声明为final,意味着它不能再派生出新的子类,不能作为父类被继承。因此一个类不能既被声明为 abstract的,又被声明为final的。将变量或方法声明为final,可以保证它们在使用中不被改变。被声明为final的变量必须在声明时给定初值,而在以后的引

实验九 TCP 协议分析实验_tcp协议分析实验_半夏风情的博客-程序员秘密

实验九 TCP 协议分析实验1.TCP 协议介绍**TCP 是传输控制协议 (Transmission Control Protocal)的缩写,提供面向连接的可靠的传输服务。在TCP/IP 体系中,HTTP、FTP、SMTP 等协议都是使用TCP 传输方式的。(1)TCP 报文格式图1 TCP 报文段格式TCP 报文分为首部和数据两个部分。如图1 所示,TCP 报文段首部的前20 字节是固定的,后面有4 ×n 字节是可选项。其中:源端口和目的端口:各2 字节,用于区分源端和目的端的多个应

解决开机提示加载PKCS#11库失败,请检查您的安装问题_pkcs#11库失败是什么意思_冬月nov25的博客-程序员秘密

不知啥时候,突然开机就显示这个错误,强迫症表示很难受啊!一开始卸载BJCA之后发现还不行,最后发现是自己没有卸载干净,哭晕o(╥﹏╥)o记住一定要把BJCA相关的全部卸载,最后还有ePass3000卸载掉!!!之后重新安装BJCA程序即可。...

ros navigation 中的amcl编译和运行_amcl roslaunch_poplar1993的博客-程序员秘密

ros 中的navigation可以根据机器人自动实现路径规划,自主定位。其中的amcl实现了一种基于蒙特卡洛的粒子滤波定位方法。本文测试环境为ubuntu18.04 + ros melodic。1. ros安装详见官方网站安装步骤:http://wiki.ros.org/melodic/Installation/Ubuntu2. navigation 安装...

随便推点

SAP认证考试新条款_fiori sap认证考试_SAPmatinal的博客-程序员秘密

特别是认证考试报名的信息更新如下:考试资格获取的规定参加SAP全球顾问资格认证考试,您需要具备如下条件并获得SAP官方培训部审核通过后,方可报名参加:1. 参加SAP官方提供的培训课程达到指定的天数,课程可以包括标准课程,顾问学院课程2. 参加SAP官方授权合作伙伴,在授权课程范围内提供的培训课程达到指定的天数3. 购买并学习了SAP Learning Hu

hyper-v 常用管理命令_weixin_33939380的博客-程序员秘密

CMD进入hyper-v提示界面:sconfigget-windowsfeature,用来查看 windows组件Add-windowsfeature功能对应命令,用来安装所需功能。get-command –module mpio获取MPIO配置命令mpiocpl 多路径管理对话磁盘管理:diskpart 磁盘管理lisk disk 查看磁盘set disk 1 选定磁盘a...

(深度学习)Keras用法_慢慢ss的博客-程序员秘密

一、Keraskeras 是一个高层神经网络API,Keras由纯python编写而成并基于Tensorflow、Theano以及CNTK后端。可以通过import keras查看keras用的什么后端,例如输出的是​​,可以在keras.json文件修改后端或者在代码中临时修改import osos.environ[‘KERAS_BACKEND’]=‘tensorflow’。二、K...

CSV纯文本数据格式_happmaoo的博客-程序员秘密

&quot;CSV&quot; 是逗号分隔文件 (Comma Separated Values)的首字母英文缩写1. 是一种用来存储数据的纯文本格式,通常用于电子表格或数据库软件。2. 在 CSV 文件中,数据&quot;栏&quot;以逗号分隔,可允许程序通过读取文件为数据重新&amp;lt;wbr&amp;gt;&amp;lt;/wbr&amp;gt;创建正确的栏结构,并在每次遇到逗号时开始新的一栏。一般格式第一行写列名(不同栏目使用逗号分割);第二行开始往...

[UE4]基于物理的材质_weixin_30522183的博客-程序员秘密

基于物理的材质可以产生更准确并且通常更加自然的外观,在所有照明环境中都可以同样完美地工作!官方说明转载于:https://www.cnblogs.com/timy/p/10049945.html...

推荐文章

热门文章

相关标签