springboot+shiro前后端分离实现!!

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

  • 一看就懂!Springboot +Shiro +VUE 前后端分离式权限管理系统_大誌的博客-CSDN博客_shiro vue
  • SpringBoot+Shiro+JWT实现权限管理_西瓜不甜柠檬不酸的博客-CSDN博客_springboot+shiro+jwt

1、前言

1.1、唠嗑部分

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

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

1.2、目标效果:

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

springboot+shiro前后端分离实现!!_第1张图片

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

springboot+shiro前后端分离实现!!_第2张图片

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

springboot+shiro前后端分离实现!!_第3张图片

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

springboot+shiro前后端分离实现!!_第4张图片

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

springboot+shiro前后端分离实现!!_第5张图片

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

springboot+shiro前后端分离实现!!_第6张图片

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

springboot+shiro前后端分离实现!!_第7张图片

springboot+shiro前后端分离实现!!_第8张图片

1.3、技术支持:

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

2、核心部分 

2.1、原理

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

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

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

springboot+shiro前后端分离实现!!_第9张图片

2.2、采坑部分

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

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

2.3、代码

pom文件


        
        
            org.springframework.boot
            spring-boot-starter-aop
        
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
        
            com.google.code.gson
            gson
            2.8.2
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
        
            mysql
            mysql-connector-java
            runtime
        
        
        
            com.auth0
            java-jwt
            3.2.0
        
        
        
            org.projectlombok
            lombok
            true
        
        
        
            com.baomidou
            mybatis-plus-boot-starter
            3.4.1
        
        
        
            org.apache.velocity
            velocity-engine-core
            2.2
        
         
        
            com.baomidou
            mybatis-plus-generator
            3.4.1
            test
        
        
        
            org.apache.shiro
            shiro-spring
            1.3.2
        
       

        
            org.springframework.boot
            spring-boot-starter-test
            test
            
                
                    org.junit.vintage
                    junit-vintage-engine
                
            
        
    

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

springboot+shiro前后端分离实现!!_第10张图片

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,BaseMapper是mybatis-plus封装好大量基本sql的一个类,直接调用指定代码即可,不用手写sql,加快开发速度。

@Repository
public interface UserMapper extends BaseMapper {

}

UserService.java

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

UserServiceImpl.java

@Service
public class UserServiceImpl extends ServiceImpl implements UserService {
    @Autowired
    UserMapper userMapper;

    @Override
    public User findByUsername(String username) {
        //相当于select * from user where account='${username}'
        QueryWrapper 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 data=new HashMap();   //数据

    //无权访问
    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 filters=new HashMap<>();
        filters.put("auth",new AuthFilter());        //自定义的认证授权过滤器
        shiroFilterFactoryBean.setFilters(filters);  //添加自定义的认证授权过滤器

        //要拦截的路径放在map里面
        Map filterMap=new LinkedHashMap();
        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 下载使用

你可能感兴趣的:(后端java,shiro,java,spring,boot)