基于spring boot+Shiro搭建的前后端分离鉴权架构

一.简介

为何选择Shiro?

Apache Shiro是一个强大且易用的Java安全框架。开发者使用shiro可以轻松完成身份验证、授权、密码和会话管理。

Shiro的主要API

基于spring boot+Shiro搭建的前后端分离鉴权架构_第1张图片

Authentication身份认证/登录,验证用户是不是拥有相应的身份;

Authorization授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

Session Manager会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;

Cryptography加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

Web SupportWeb支持,可以非常容易的集成到Web环境;

Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;

Concurrencyshiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

Testing提供测试支持;

Run As允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

Remember Me记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

Shiro如何工作?

基于spring boot+Shiro搭建的前后端分离鉴权架构_第2张图片

Subject主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;

SecurityManager安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;

Realm域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。

 

二.核心代码

1.登录/登出功能

假设我们有两个用户:

 

admin1: 用户名/密码为admin1/admin时成功,返回身份信息以及sessionId;

admin2: 用户名/密码为admin2/admin时成功,返回身份信息以及sessionId;

之后需要权限控制的接口必须在http请求头header带上sessionId(Authorization:sessionId的格式)。

基于spring boot+Shiro搭建的前后端分离鉴权架构_第3张图片

登录/登出代码:

@PostMapping("/login")
    public Object login(@RequestBody LoginVo loginVo) {
        //得到Subject及创建用户名/密码身份验证Token(即用户身份/凭证)
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(loginVo.getAccount(), loginVo.getPassword());
        token.setRememberMe(false);
        if (subject.isAuthenticated()) {
            subject.logout();
        }
        try {
            //登录,即身份验证
            subject.login(token);
            Session session = subject.getSession();
            User user = User.loginUser();
            user.setFlag(loginVo.getFlag());
            user.setSessionId(session.getId());
            //返回一个sessionId
            return user;
        } catch (UnknownAccountException e){
            return "账号/密码错误";
        }
        catch (AuthenticationException e) {
            //身份验证失败
            return "程序错误";
        }
    }

    @PostMapping("/logout")
    public Object logout() {
        try {
            Subject subject = SecurityUtils.getSubject();
            subject.logout();
            return "成功退出登录!";
        } catch (Exception e) {
            return "退出登录失败!";
        }
    }

 

2.Shiro配置

配置shiro的拦截器,以及开启@RequiresPermissions 注解支持

package com.lee.config;


import com.lee.filter.ApiPathPermissionFilter;
import com.lee.shiro.MyRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 *
 * 功能描述: shiro配置类
 *
 * @param:
 * @return:
 * @auther: liyiyu
 * @date: 2020/3/17 17:05
 */
@Configuration
public class ShiroConfig {

    /**
     * 设置/login /logout 两个请求可以任意访问
     */
    @Bean
    public ShiroFilterFactoryBean factory(SecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        factoryBean.setSecurityManager(securityManager);

        // 自定义拦截器
        Map filterMap = new LinkedHashMap<>();
        filterMap.put("apiPathPermissionFilter", new ApiPathPermissionFilter());
        factoryBean.setFilters(filterMap);

        Map filterRuleMap = new LinkedHashMap<>();
        //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
        filterRuleMap.put("/logout", "logout");
        // 配置不会被拦截的链接 顺序判断
        filterRuleMap.put("/login", "anon");
        // 其他请求通过我们自己的apiPathPermissionFilter
        filterRuleMap.put("/*", "apiPathPermissionFilter");
        filterRuleMap.put("/**", "apiPathPermissionFilter");
        filterRuleMap.put("/*.*", "apiPathPermissionFilter");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);

        return factoryBean;
    }

    /**
     * SecurityManager安全管理器,是Shiro的核心
     */
    @Bean
    public SecurityManager securityManager(MyRealm myRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //设置session生命周期
        securityManager.setSessionManager(sessionManager());
        // 设置自定义 realm
        securityManager.setRealm(myRealm);
        return securityManager;
    }

    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        // https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /**
     * 开启aop注解支持
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    /**
     * 自定义sessionManager
     */
    @Bean
    public SessionManager sessionManager() {
        //由shiro管理session,每次访问后会重置过期时间
        MySessionManager mySessionManager = new MySessionManager();
        //设置过期时间,单位:毫秒
        mySessionManager.setGlobalSessionTimeout(MySessionManager.DEFAULT_GLOBAL_SESSION_TIMEOUT);
        return mySessionManager;
    }


    /**
     * LifecycleBeanPostProcessor将Initializable和Destroyable的实现类统一在其内部,
     * 自动分别调用了Initializable.init()和Destroyable.destroy()方法,从而达到管理shiro bean生命周期的目的
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }


}

2.sessionId获取

实现自己的一套session管理器,需重写DefaultWebSessionManager
package com.lee.config;

import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

/**
 *
 * 功能描述: 自定义sessionId获取,用于请求头中传递sessionId,并让shiro获取判断权限
 * 想实现自己的一套session管理器,需继承DefaultWebSessionManager来重写
 *
 * @param:
 * @return:
 * @auther: liyiyu
 * @date: 2020/3/17 17:05
 */
public class MySessionManager extends DefaultWebSessionManager {

    private static final String AUTHORIZATION = "Authorization";

    public MySessionManager() {
        super();
    }


    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        //修改shiro管理sessionId的方式,改为获取请求头,前端时header必须带上 Authorization:sessionId
        return WebUtils.toHttp(request).getHeader(AUTHORIZATION);
    }
}

3.登录实现以及权限控制

doGetAuthenticationInfo是登录的具体实现,通过查询数据库匹配账号和加密后的密码来判断是否正确

doGetAuthorizationInfo是权限判断的具体实现,通过获取当前请求的用户来判断用户的权限

我们设置admin1 拥有poetry1 poetry2的权限,admin2拥有poetry3 poetry4的权限

package com.lee.shiro;

import com.lee.entity.User;
import com.lee.util.PasswordUtils;
import org.apache.shiro.SecurityUtils;
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.Value;
import org.springframework.stereotype.Component;

import java.util.*;

/**
 *
 * 功能描述:自定义用户认证授权类
 *
 * @param:
 * @return:
 * @auther: liyiyu
 * @date: 2020/3/17 17:04
 */
@Component
public class MyRealm extends AuthorizingRealm {

    @Value("${password_salt}")
    private String salt;

    /**
     * AuthorizationInfo 用于聚合授权信息
     * 会判断@RequiresPermissions 里的值是否
     *
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //进入数据库查询拥有的权限查询
        List list = new ArrayList<>();
        User user = (User)SecurityUtils.getSubject().getPrincipal();
        if ("1".equals(user.getRoleId())){
            String[] array = {"poetry1","poetry2"};
            list  = Arrays.asList(array);
        }else if ("2".equals(user.getRoleId())){
            String[] array = {"poetry3","poetry4"};
            list  = Arrays.asList(array);
        }

        Set set = new HashSet(list);
        info.addStringPermissions(set);
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String account = (String) authenticationToken.getPrincipal();  //得到用户名
        String pwd = new String((char[]) authenticationToken.getCredentials()); //得到密码
        //将密码进行加密处理,与数据库加密后的密码进行比较
        String inPasswd = PasswordUtils.entryptPasswordWithSalt(pwd, salt);
        //通过数据库验证账号密码,成功的话返回一个封装的ShiroUser实例
        String saltPasswd = PasswordUtils.entryptPasswordWithSalt("admin", salt);
        User user = null;
        //这里要注意返回用户信息尽可能少,返回前端所需要的用户信息就可以了
        if ("admin1".equals(account) && saltPasswd.equals(inPasswd)) {
            user = new User();
            user.setUid("1");
            user.setUname("用户一");
            user.setEid(1);
            user.setDeptName("祖安大区");
            user.setDeptId("1");
            user.setRoleId("1");
            user.setRoleName("祖安文科状元");
        }else if ("admin2".equals(account) && saltPasswd.equals(inPasswd)){
            user = new User();
            user.setUid("1");
            user.setUname("用户二");
            user.setEid(1);
            user.setDeptName("祖安大区");
            user.setDeptId("1");
            user.setRoleId("2");
            user.setRoleName("祖安理科状元");
        }
        if (user != null) {
            //如果身份认证验证成功,返回一个AuthenticationInfo实现;
            return new SimpleAuthenticationInfo(user, pwd, getName());
        } else {
            //错误的帐号
            throw new UnknownAccountException();
        }
    }
}

4.跨域问题

在开发过程中出现了跨域的问题

package com.lee.config;

import com.lee.interceptor.UrlInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

/**
 *
 * 功能描述: MVC拦截器配置
 *
 * @param:
 * @return:
 * @auther: liyiyu
 * @date: 2020/3/17 17:06
 */
@Configuration
public class MvcConfiguration extends WebMvcConfigurerAdapter {


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //为所有请求处理跨域问题
        registry.addInterceptor(new UrlInterceptor()).addPathPatterns("/**");
        super.addInterceptors(registry);
    }

}

跨域解决办法

package com.lee.interceptor;

import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 *
 * 功能描述: url拦截器,处理前后端分离跨域问题
 *
 * @param:
 * @return:
 * @auther: liyiyu
 * @date: 2020/3/17 17:04
 */
public class UrlInterceptor extends HandlerInterceptorAdapter {

	@Override
	public boolean preHandle(HttpServletRequest request,
							 HttpServletResponse response, Object handler) throws Exception {
		//允许跨域,不能放在postHandle内
		response.setHeader("Access-Control-Allow-Credentials", "true");
		String str = request.getHeader("origin");
		response.setHeader("Access-Control-Allow-Origin", request.getHeader("origin"));
		response.setHeader("Cache-Control", "no-cache");
		response.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, PUT");
		response.setHeader("Access-Control-Max-Age", "0");
		response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept,Authorization,WG-App-Version, WG-Device-Id, WG-Network-Type, WG-Vendor, WG-OS-Type, WG-OS-Version, WG-Device-Model, WG-CPU, WG-Sid, WG-App-Id, WG-Token");
		response.setHeader("XDomainRequestAllowed", "1");
		return true;
	}
}

5.验证权限

通过@RequiresPermissions 来验证权限(ShiroConfig类配置)

    @GetMapping("/poetry1")
    @RequiresPermissions("poetry1")
    public Object poetry1(){
        return "床前明月光";
    }

    @GetMapping("/poetry2")
    @RequiresPermissions("poetry2")
    public Object poetry2(){
        return "疑是地上霜";
    }

    @GetMapping("/poetry3")
    @RequiresPermissions("poetry3")
    public Object poetry3(){
        return "举头望明月";
    }

    @GetMapping("/poetry4")
    @RequiresPermissions("poetry4")
    public Object poetry4(){
        return "低头思故乡";
    }

具体代码:https://github.com/leeyiyu/shiro_token

参考文档:https://www.iteye.com/blog/jinnianshilongnian-2018398

你可能感兴趣的:(基于spring boot+Shiro搭建的前后端分离鉴权架构)