Spring Boot Shiro

欢迎光临我的博客[http://poetize.cn],前端使用Vue2,聊天室使用Vue3,后台使用Spring Boot

Shiro 核心 API

Subject:用户主体(每次请求都会创建Subject)。

	principal:代表身份。可以是用户名、邮件、手机号码等等,用来标识一个登录主体的身份。

	credential:代表凭证。常见的有密码,数字证书等。

SecurityManager:安全管理器(关联 Realm),用于安全校验。

Realm:Shiro 连接数据的桥梁。


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

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

CacheManager:缓存控制器,来管理如用户、角色、权限等缓存的控制器。

SessionManager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中。

SessionDAO:会话储存。

Shiro 认证与授权

身份认证:

	Step1:应用程序代码调用 Subject.login(token) 方法后,传入代表最终用户身份的 AuthenticationToken 实例 Token。

	Step2:将 Subject 实例委托给应用程序的 SecurityManager(Shiro 的安全管理)并开始实际的认证工作。

	Step3、4、5:SecurityManager 根据具体的 Realm 进行安全认证。

权限认证(涉及到三张表:用户表、角色表和权限表):

	权限(Permission):即操作资源的权利(添加、修改、删除、查看操作的权利)。
	
	角色(Role):指的是用户担任的角色,一个角色可以有多个权限。
	
	用户(User):在 Shiro 中,代表访问系统的用户,即上面提到的 Subject 认证主体。


请求步骤:

	浏览器发出第一次请求的时候,去redis里找不到对应的session,会进入到登录页。
	
	当在登录页输入完正确的账号密码后,才能登录成功。

	根据登录成功后的session生成sessionId,并传到前端浏览器中,浏览器以cookie存储,同时将session存储到redis中。
	
	每次浏览器访问后台,都会刷新session的过期时间expireTime。
	
	当浏览器再次请求时,将当前浏览器中的所有的cookie设置到request headers请求头中。
	根据传入的sessionId串到共享的redis存储中匹配。
	如果匹配不到,则会跳转到登录页,如果匹配成功,则会访问通过。

Spring Boot Shiro 依赖

<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
</dependency>

自定义 Realm

自定义 Realm 需要继承 AuthorizingRealm 类,该类封装了很多方法,且继承自 Realm 类。

重写以下两个方法:
	doGetAuthenticationInfo() 方法:用来验证当前登录的用户,获取认证信息。
	doGetAuthorizationInfo() 方法:为当前登录成功的用户授予权限和分配角色。

public class CustomRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    /**
     * 登录成功的用户授予权限和分配角色
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //获取用户名
        String account = (String) principals.getPrimaryPrincipal();

        //从数据库查询用户角色信息
        User user = userService.getUserByAccount(account);

        //设置角色
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        Set<String> roles = new HashSet();
        if (user.getAdmin()) {
            roles.add(Base.ROLE_ADMIN);
        }
        authorizationInfo.setRoles(roles);

        return authorizationInfo;
    }

    /**
     * 执行认证逻辑
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //获取用户名
        String account = (String) token.getPrincipal();

        //从数据库查询该用户
        User user = userService.getUserByAccount(account);

        if (null == user) {
            throw new UnknownAccountException();    //没找到该帐号
        }
        if (UserStatus.blocked.equals(user.getStatus())) {
            throw new LockedAccountException();	//帐号锁定
        }

        //传入用户名和密码进行身份认证,并返回认证信息
        return new SimpleAuthenticationInfo(
                user.getAccount(),
                user.getPassword(),
                ByteSource.Util.bytes(user.getSalt()),  //盐(密码加盐加密处理)
                getName()
        );
    }
}

自定义 SessionDAO

Cachemanager缓存里可以包含权限认证的缓存、用户及权限信息的缓存等,也可以做Session缓存。

SessionDAO是做Session持久化的,可以使用Redis来存储。

默认 SessionDAO:MemorySessionDAO:

	将Session保存在内存中,存储结构是ConcurrentHashMap。

	public class MemorySessionDAO extends AbstractSessionDAO {

	    private ConcurrentMap<Serializable, Session> sessions = new ConcurrentHashMap();

	    protected Serializable doCreate(Session session) {
	        Serializable sessionId = this.generateSessionId(session);
	        this.assignSessionId(session, sessionId);
	        this.storeSession(sessionId, session);
	        return sessionId;
	    }
	}


/**
 * 将Session保存到Redis
 */
public class CustomSessionDAO extends CachingSessionDAO {

    @Autowired
    private RedisTemplate redisTemplate;

    //默认缓存过期时间:30分钟
    public final static long DEFAULT_EXPIRE = 60 * 30;

    @Override
    protected Serializable doCreate(Session session) {
        //创造SessionId
        Serializable sessionId = generateSessionId(session);
        //注册SessionId
        assignSessionId(session, sessionId);
        //缓存Session
        redisTemplate.opsForValue().set(sessionId.toString(), session, DEFAULT_EXPIRE, TimeUnit.SECONDS);
        return sessionId;
    }

    @Override
    protected void doUpdate(Session session) {
        if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
            //会话过期/停止
            return;
        }
        redisTemplate.opsForValue().set(session.getId().toString(), session, DEFAULT_EXPIRE, TimeUnit.SECONDS);
    }

    @Override
    protected void doDelete(Session session) {
        redisTemplate.delete(session.getId().toString());
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        return (Session) redisTemplate.opsForValue().get(sessionId.toString());
    }
}

自定义 SessionManager(待完善)

public class CustomSessionManager extends DefaultWebSessionManager {

    public static final String TOKEN = "token";

    /**
     * 调用登陆接口的时候,是没有token的。
     * 登陆成功后,产生了token,我们把它放到request中。
     * 返回结果给客户端的时候,把它从request中取出来,并且传递给客户端。
     * 客户端每次带着这个token过来,就相当于是浏览器的cookie的作用,也就能维护会话了。
     */
    @Override
    public Serializable getSessionId(SessionKey key) {
        Serializable sessionId = key.getSessionId();
        if(sessionId == null && WebUtils.isWeb(key)){
            HttpServletRequest request = WebUtils.getHttpRequest(key);
            HttpServletResponse response = WebUtils.getHttpResponse(key);
            sessionId = this.getSessionId(request,response);
        }
        HttpServletRequest request = WebUtils.getHttpRequest(key);
        request.setAttribute(TOKEN,sessionId.toString());
        return sessionId;
    }

    /**
     * DefaultWebSessionManager默认实现中,是通过Cookie确定SessionId。
     * 重写时,只需要把获取SessionId的方式变更为在request header中获取即可。
     */
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String id = httpRequest.getHeader(TOKEN);

        if (!StringUtils.isEmpty(id)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        }
        return super.getSessionId(request, response);
    }
}

配置类 ShiroConfig

@Configuration
public class ShiroConfig {

    /**
     * 创建 ShiroFilterFactoryBean
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        
        //设置securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        //LinkedHashMap 是有序的,进行顺序拦截器配置
        Map<String, String> filterMap = new LinkedHashMap();
        filterMap.put("/static/**", "anon");	//无需认证可以访问
        filterMap.put("/login", "anon");
        filterMap.put("/register", "anon");

        //配置退出过滤器,其中具体的退出代码Shiro已经替我们实现了,登出后跳转配置的LoginUrl
        filterMap.put("/logout", "logout");

        filterMap.put("/**/create", "authc");	//必须认证才可以访问
        filterMap.put("/**/update", "authc");
        filterMap.put("/**/delete", "authc");
        filterMap.put("/upload", "authc");

        filterMap.put("/admin", "perms[admin]");	//资源必须得到资源权限才能访问,多个参数写法:perms["admin,user"]
        filterMap.put("/admin", "role[admin]");	//资源必须得到角色权限才能访问

        filterMap.put("/**", "anon");


        //设置默认登录的URL,身份认证失败会访问该URL
        shiroFilterFactoryBean.setLoginUrl("/login");
        //身份认证设置成功之后要跳转的URL
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //设置未授权界面,权限认证失败会访问该URL
        shiroFilterFactoryBean.setUnauthorizedUrl("/unAuthorized");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 配置安全管理器:DefaultWebSecurityManager
     */
    @Bean
    public SecurityManager securityManager(CustomRealm realm, SessionManager sessionManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        securityManager.setSessionManager(sessionManager);
        return securityManager;
    }

    /**
     * 创建自定义 Realm
     */
    @Bean
    public CustomRealm customRealm() {
        CustomRealm shiroRealm = new CustomRealm();
        //配置密码加密
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("md5");    //加密方式
        matcher.setHashIterations(2);   //加密次数
        shiroRealm.setCredentialsMatcher(matcher);
        return shiroRealm;
    }

    /**
     * 配置 SessionManager
     */
    @Bean
    public SessionManager sessionManager() {
        CustomSessionManager customSessionManager = new CustomSessionManager();
        //session过期时间:1小时(默认半小时)
        customSessionManager.setGlobalSessionTimeout(60 * 60 * 1000);
        customSessionManager.setSessionDAO(new CustomSessionDAO());
        return customSessionManager;
    }

    /**
     * Spring Boot Shiro 开启注释
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

Session的查询、刷新

SimpleSession几个属性:

	Serializable id:session id;

	Date startTimestamp:session的创建时间;

	Date stopTimestamp:session的失效时间;

	Date lastAccessTime:session的最近一次访问时间,初始值是startTimestamp

	long timeout:session的有效时长,默认30分钟

	boolean expired:session是否到期

	Map<Object, Object> attributes:session的属性容器


查询:Session session = SecurityUtils.getSubject().getSession();	//返回的就是绑定在当前subjuct的session。

刷新:SimpleSession的touch()

	public void touch() {
		this.lastAccessTime = new Date();
	}

	Web应用,每次进入ShiroFilter都会自动调用session.touch()来更新最后访问时间。

	过期时间判断:当前时间-lastAccessTime=是否超过有效时长。

Shiro支持三种方式的授权

1、编程式,通过写if/else授权代码块

	Subject subject = SecurityUtils.getSubject();
	if(subject.hasRole("admin")) {
	    // 有权限,执行相关业务
	} else {
	    // 无权限,给相关提示
	}

2、注解式,通过在执行的Java方法上放置相应的注解完成

	@RequiresPermissions("admin")
	public List<User> listUser() {
	    // 有权限,获取数据  
	}

3、JSP/GSP标签,在JSP/GSP页面通过相应的标签完成

	<shiro:hasRole name="admin">
	    <!-- 有权限 -->
	</shiro:hasRole>

Spring Boot 集成 Shiro 和 Ehcache(待完善)

引入配置文件 ehcache.xml:

	application.xml配置文件添加:spring.cache.ehcache.config=classpath:ehcache.xml

	<?xml version="1.0" encoding="UTF-8"?>
	<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
	         updateCheck="false">
	    
	    <diskStore path="java.io.tmpdir/shiro-cache"/>

	    <defaultCache	//默认缓存策略
	            eternal="false"	//对象是否永久有效,一但设置了,timeout将不起作用
	            maxElementsInMemory="1000"	//缓存最大元素数目
	            overflowToDisk="false"	//当内存中对象数量达到maxElementsInMemory时,Ehcache将对象写到磁盘中。
	            diskPersistent="false"	//是否在磁盘上持久化。指重启JVM后,数据是否有效。默认为false
	            timeToIdleSeconds="0"	//设置对象在失效前的允许闲置时间(单位:s),默认是0(永久有效)。
	            timeToLiveSeconds="600"	//设置对象在失效前允许存活时间(单位:s),默认是0(永久有效)。
	            //当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存
	            //LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)
	            memoryStoreEvictionPolicy="LRU" />
	 
	    <cache
	            name="users"	//缓存名称
	            eternal="false"
	            maxElementsInMemory="500"
	            overflowToDisk="false"
	            diskPersistent="false"
	            timeToIdleSeconds="0"
	            timeToLiveSeconds="300"
	            memoryStoreEvictionPolicy="LRU" />
	</ehcache>

Controller 与 Service 使用

Controller:

    @PostMapping("/login")
    public Result login(@RequestBody User user) {
        Result r = new Result();
        //获取Subject用户主体
        Subject subject = SecurityUtils.getSubject();
        //封装用户数据
        UsernamePasswordToken token = new UsernamePasswordToken(user.getAccount(), user.getPassword());
        try {
            //执行认证操作(调用UserRealm中的方法认证)
            subject.login(token);
            //认证通过
            User currentUser = userService.getUserByAccount(user.getAccount());
            subject.getSession().setAttribute(Base.CURRENT_USER, currentUser);
            r.setResultCode(ResultCode.SUCCESS);
            r.getData().put("token", subject.getSession().getId());
        } catch (UnknownAccountException e) {
            r.setResultCode(ResultCode.USER_NOT_EXIST); //用户不存在
        } catch (LockedAccountException e) {
            r.setResultCode(ResultCode.USER_ACCOUNT_FORBIDDEN); //账号被锁定
        }catch (IncorrectCredentialsException e) {
            r.setResultCode(ResultCode.USER_LOGIN_PASSWORD_ERROR);   //密码错误
        } catch (AuthenticationException e) {
            r.setResultCode(ResultCode.USER_LOGIN_ERROR);   //认证错误(包含以上错误)
        }
        return r;
    }

    @PostMapping("/register")
    public Result register(@RequestBody User user) {
        Result r = new Result();

        User temp = userService.getUserByAccount(user.getAccount());
        if (null != temp) {
            r.setResultCode(ResultCode.USER_HAS_EXISTED);
            return r;
        }

        userService.saveUser(user);

		r.setResultCode(ResultCode.SUCCESS);
        return r;
    }

Service:

    @Override
    @Transactional
    public void saveUser(User user) {
    	//密码加密
        String newPassword = new SimpleHash(
                "md5",user.getPassword(),
                ByteSource.Util.bytes("salt"),2).toHex();

        user.setPassword(newPassword);

        return userRepository.save(user);
    }
posted @ 2019-12-24 14:16  LittleDonkey  阅读(1348)  评论(0编辑  收藏  举报