权限模块基本流程
权限模块的基本流程:用户申请账号和权限 -->登陆认证 -->安全管控模块认证 -->调用具体权限模块(基于角色的权限控制) --> 登陆成功 -->访问资源 -->安全模块鉴权 -->通过后获取资源。整个流程如下图
常用的两个安全管控模块比较
JAAS,java验证和授权模块,jdk提供的一套标准的方法,对于有异构分布式的大型企业推荐这个框架,很多开源项目的安全框架就是JAAS,例如CAS 、tomcat、activemq等。
spring security,市场占有率最高的框架,支持范围广、功能更强大,但是学习比较复杂,成本比较高,跟spring框架绑定,无法在非spring项目中使用,对于安全需求复杂、支持范围广的项目可以使用。
shiro,简单易学,功能对于一般的web项目已经足够,相对独立,可用于非Spring的项目。
shiro基础知识
shiro扩展性很强,我们可以实现自己的realms、sessionDao、cachemanager以及自定义过滤器来实现个性化需求,其架构图如下:
shiro的核心概念有:
subject:用户,这个用户不一定使人,而是指任何与当前系统交互的对象
principal:身份标示,表示主体的抽象概念,可以用来表示任何实体,如username、ID、phone等
credentail:密码
token:principal+ credentail,用户在登陆时提交的
securityManager:安全管理器,shiro的核心模块,查看源码就可以从这个类开始看
authenticator:认证器,负责主体认证,其需要认证策略,决定什么用户可以认证通过
authorizer:鉴权器,决定用户是否有权限访问某个资源
releam:安全实体数据源,自带了多种实现,我们也可以扩展自己的
role:角色,权限和用户的桥梁
permissions:权限,shiro权限描述,printer:print:A,第一个代表域,第二个代表操作,第三个代表实例
shiro自带的过滤器实现:
springboot2.x整合shiro
项目依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
</dependencies>
application.yml
spring: datasource: url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 username: root password: 1234 driver-class-name: com.mysql.cj.jdbc.Driver jpa: database: mysql show-sql: true hibernate: ddl-auto: update properties: hibernate: dialect: org.hibernate.dialect.MySQL5Dialect thymeleaf: cache: false mode: HTML
自定义的安全实体数据源
package com.jlwj.shiro.config; import com.jlwj.shiro.model.SysPermission; import com.jlwj.shiro.model.SysRole; import com.jlwj.shiro.model.UserInfo; import com.jlwj.shiro.service.UserInfoService; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; 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.apache.shiro.util.ByteSource; import javax.annotation.Resource; @Slf4j public class MyShiroRealm extends AuthorizingRealm { @Resource private UserInfoService userInfoService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { log.info("principals:{}",principals); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); UserInfo userInfo = (UserInfo)principals.getPrimaryPrincipal(); for(SysRole role:userInfo.getRoleList()){ authorizationInfo.addRole(role.getRole()); for(SysPermission p:role.getPermissions()){ authorizationInfo.addStringPermission(p.getPermission()); } } return authorizationInfo; } /*主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。*/ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { log.info("token:{}",token); //获取用户的输入的账号. String username = (String)token.getPrincipal(); System.out.println(token.getCredentials()); //通过username从数据库中查找 User对象,如果找到,没找到. //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法 UserInfo userInfo = userInfoService.findByUsername(username); // log.info("userInfo:{}",userInfo); if(userInfo == null){ return null; } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( userInfo, //用户名 userInfo.getPassword(), //密码 ByteSource.Util.bytes(userInfo.getCredentialsSalt()),//salt getName() //realm name ); return authenticationInfo; } public void clearCached() { PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals(); super.clearCache(principals); } }
自定义的过滤器,用于验证码校验
package com.jlwj.shiro.filter; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; /** * @author hehang on 2019-08-14 * @description sdf */ @Slf4j public class CustomFormAuthenticationFilter extends FormAuthenticationFilter { @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { log.info("自定义过滤器"); //在这里进行验证码的校验 //从session获取正确验证码 HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpSession session =httpServletRequest.getSession(); //取出session的验证码(正确的验证码) String validateCode = (String) session.getAttribute("validateCode"); log.info("session:{}",validateCode); //取出页面的验证码 //输入的验证和session中的验证进行对比 String randomcode = httpServletRequest.getParameter("randomcode"); String username = httpServletRequest.getParameter("username"); log.info("randomcode:{}",randomcode); if(randomcode!=null && validateCode!=null && !randomcode.equals(validateCode)){ //如果校验失败,将验证码错误失败信息,通过shiroLoginFailure设置到request中 httpServletRequest.setAttribute("shiroLoginFailure", "randomCodeError"); //拒绝访问,不再校验账号和密码 return true; } //验证码通过,才会到用户认证否则直接到controller层 return super.onAccessDenied(request, response); } }
shiro核心配置
package com.jlwj.shiro.config; import com.jlwj.shiro.filter.CustomFormAuthenticationFilter; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.cache.CacheManager; import org.apache.shiro.cache.ehcache.EhCacheManager; import org.apache.shiro.crypto.hash.SimpleHash; import org.apache.shiro.mgt.RememberMeManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.util.ByteSource; import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; import org.apache.shiro.web.mgt.CookieRememberMeManager; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.web.servlet.SimpleCookie; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; import javax.servlet.Filter; import java.util.LinkedHashMap; import java.util.Map; import java.util.Properties; /** * @author hehang on 2019-08-13 * @description sd */ @Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, Filter> filterMap = new LinkedHashMap<>(); filterMap.put("authc",formAuthenticationFilter()); shiroFilterFactoryBean.setFilters(filterMap); //拦截器. Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>(); //静态资源放行 filterChainDefinitionMap.put("/static/**", "anon"); //配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了 filterChainDefinitionMap.put("/logout", "logout"); //所有请求必须登陆 filterChainDefinitionMap.put("/**", "authc"); // 设置登陆链接 shiroFilterFactoryBean.setLoginUrl("/login"); // 登录成功后要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/index"); // 未授权链接 shiroFilterFactoryBean.setUnauthorizedUrl("/refuse"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * 凭证匹配器 * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了 * ) * @return */ @Bean public FormAuthenticationFilter formAuthenticationFilter(){ FormAuthenticationFilter formAuthenticationFilter = new CustomFormAuthenticationFilter(); formAuthenticationFilter.setPasswordParam("password"); formAuthenticationFilter.setUsernameParam("username"); formAuthenticationFilter.setRememberMeParam("rememberMe"); return formAuthenticationFilter; } @Bean public HashedCredentialsMatcher hashedCredentialsMatcher(){ HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法; hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5("")); return hashedCredentialsMatcher; } @Bean public MyShiroRealm myShiroRealm(){ MyShiroRealm myShiroRealm = new MyShiroRealm(); myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myShiroRealm; } @Bean public SecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setCacheManager(cacheManager()); securityManager.setSessionManager(sessionManager()); securityManager.setRememberMeManager(rememberMeManager()); securityManager.setRealm(myShiroRealm()); return securityManager; } @Bean public CacheManager cacheManager(){ EhCacheManager cacheManager = new EhCacheManager(); cacheManager.setCacheManagerConfigFile("classpath:shiro-ehcache.xml"); return cacheManager; } @Bean public SessionManager sessionManager(){ DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setGlobalSessionTimeout(600000); sessionManager.setDeleteInvalidSessions(true); return sessionManager; } @Bean public RememberMeManager rememberMeManager(){ CookieRememberMeManager rememberMeManager = new CookieRememberMeManager(); rememberMeManager.setCookie(rememberCookie()); return rememberMeManager; } @Bean public SimpleCookie rememberCookie(){ SimpleCookie simpleCookie = new SimpleCookie("rememberMe"); simpleCookie.setMaxAge(2592000); return simpleCookie; } /** * 开启shiro aop注解支持. * 使用代理方式;所以需要开启代码支持; * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } //注册时加密方法 public static final String md5(String password, String salt){ //加密方式 String hashAlgorithmName = "MD5"; //盐:为了即使相同的密码不同的盐加密后的结果也不同 ByteSource byteSalt = ByteSource.Util.bytes(salt); //密码 Object source = password; //加密次数 int hashIterations = 2; SimpleHash result = new SimpleHash(hashAlgorithmName, source, byteSalt, hashIterations); return result.toString(); } }
MainController
package com.jlwj.shiro.controller; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpServletRequest; import java.util.Map; import java.util.Random; @Controller public class MainController { @RequestMapping({"/","/index"}) public String index(){ return"/index"; } @RequestMapping("/asd") public String asd(){ return"/asd"; } @RequestMapping("/login") public String login(HttpServletRequest request, Map<String, Object> map) throws Exception{ System.out.println("HomeController.login()"); // 登录失败从request中获取shiro处理的异常信息。 // shiroLoginFailure:就是shiro异常类的全类名. String exception = (String) request.getAttribute("shiroLoginFailure"); System.out.println("exception=" + exception); String msg = ""; if (exception != null) { if (UnknownAccountException.class.getName().equals(exception)) { System.out.println("UnknownAccountException -- > 账号不存在:"); msg = "UnknownAccountException -- > 账号不存在:"; } else if (IncorrectCredentialsException.class.getName().equals(exception)) { System.out.println("IncorrectCredentialsException -- > 密码不正确:"); msg = "IncorrectCredentialsException -- > 密码不正确:"; } else if ("kaptchaValidateFailed".equals(exception)) { System.out.println("kaptchaValidateFailed -- > 验证码错误"); msg = "kaptchaValidateFailed -- > 验证码错误"; } else { msg = "else >> "+exception; System.out.println("else -- >" + exception); } } map.put("msg", msg); String code = String.valueOf(new Random().nextInt(1000000)); request.getSession().setAttribute("validateCode",code); map.put("code",code); // 此方法不处理登录成功,由shiro进行处理 return "/login"; } @RequestMapping("/refuse") public String unauthorizedRole(){ return "403"; } }
UserInfoController,代表资源
package com.jlwj.shiro.controller; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/userInfo") public class UserInfoController { /** * 用户查询. * @return */ @RequestMapping("/userList") @RequiresPermissions("userInfo:view")//权限管理; public String userInfo(){ return "userInfo"; } /** * 用户添加; * @return */ @RequestMapping("/userAdd") @RequiresPermissions("userInfo:add")//权限管理; public String userInfoAdd(){ return "userInfoAdd"; } /** * 用户删除; * @return */ @RequestMapping("/userDel") @RequiresPermissions("userInfo:del")//权限管理; public String userDel(){ return "userInfoDel"; } }
encache配置文件
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd"> <!--diskStore:缓存数据持久化的目录 地址 --> <diskStore path="/Users/hehang/tools/" /> <defaultCache maxElementsInMemory="1000" maxElementsOnDisk="10000000" eternal="false" overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="120" timeToLiveSeconds="120" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> </defaultCache> </ehcache>
持久化框架使用jpa,实体类要实现序列化接口,同时注意tostring方法的生成,排除调集合属性,这里就不贴出了
login.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Login</title> </head> <body> 验证码:<h4 th:text="${code}"></h4> <form action="" method="post"> <p>账号:<input type="text" name="username" value="admin"/></p> <p>密码:<input type="text" name="password" value="123456"/></p> <p>验证码:<input type="text" name="randomcode" value="1234"/></p> <p><input type="checkbox" name="rememberMe" />自动登陆</p> <p><input type="submit" value="登录"/></p> </form> 错误信息:<h4 th:text="${msg}"></h4> </body> </html>
至此,实现了基本权限模块,包括了验证码校验、remember me功能和密码加密功能。