概述
4A(认证Authentication、授权Authorization、账号Account、审计Audit)是现代任何IT系统中很基础但非常重要的部分,无论是传统管理信息系统还是互联网项目,出于保护业务数据和应用自身的安全,都会设计自己的登录和资源授权策略。最近项目中需要登录和权限相关的功能,项目为spring-boot工程,现在流行的权限验证框架有shiro和spring-security,shiro相对spring-security来说学习难度要低一点,也是比较成熟的产品,因此选择shiro作为项目的权限验证框架。
步骤
添加依赖
spring boot的版本为2.1.7.RELEASE。如果大量依赖spring的项目,可以用https://start.spring.io/
patchca是验证码部分
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.7.RELEASE</version> </parent>
shiro-spring是用的最新的版本。patchca是用于验证码。
<dependencies> <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.1</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-data-jpa</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>com.github.bingoohuang</groupId> <artifactId>patchca</artifactId> <version>0.0.1</version> </dependency> </dependencies>
配置SecurityManager
在spring boot项目中去掉了复杂的各种xml配置,改为在Java文件中配置各种bean
@Bean(name = "securityManager") public org.apache.shiro.mgt.SecurityManager defaultWebSecurityManager(@Autowired UserRealm userRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 关联realm securityManager.setRealm(userRealm); securityManager.setRememberMeManager(rememberMeManager()); return securityManager; }
配置ShiroFilterFactoryBean
可以添加Filter,以及各种资源的权限类型anon、authc、user、perms、role。ShiroFilterFactoryBean(该类实现了FactoryBean接口,在IOC容器的基础上给Bean的实现加上了一个简单工厂模式和装饰模式 我们可以在getObject()方法中灵活配置和扩展)
/** * 创建ShiroFilterFactoryBean shiro过滤bean */ @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Autowired org.apache.shiro.mgt.SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); /** * anon: 无需认证(登录)可以访问 * authc: 必须认证才可以访问 * user: 如果使用rememberMe功能可以直接访问 * perms: 该资源必须得到资源权限才可以访问 * role: 该资源必须得到角色权限才可以访问 */ Map<String, String> filerMap = new LinkedHashMap<>(); // 顺序的map filerMap.put("/login", "anon"); filerMap.put("/validCode", "anon"); filerMap.put("/**", "authc"); shiroFilterFactoryBean.setLoginUrl("/user/login.html"); shiroFilterFactoryBean.setUnauthorizedUrl("/noAuth"); shiroFilterFactoryBean.setSuccessUrl("/index"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filerMap); return shiroFilterFactoryBean; }
创建和配置Realm
public class UserRealm extends AuthorizingRealm { @Autowired private UserService userService; /** * 执行授权逻辑 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行授权逻辑1"); //给资源进行授权,这里暂时写死,实际需要从数据库中获取当前用户的资源权限 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addStringPermission("user:add"); info.addStringPermission("user:update"); return info; } /** * 执行认证逻辑 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { ValidCodeUserPassWordToken token = (ValidCodeUserPassWordToken)authenticationToken; String validCode = token.getValidCode(); if(StringUtils.isEmpty(validCode)) { throw new AuthenticationException("未输入验证码"); } //校验码部分 Subject subject = SecurityUtils.getSubject(); ValidationCode oldValidCode = (ValidationCode); subject.getSession().getAttribute("VALIDCODE"); subject.getSession().removeAttribute("VALIDCODE"); if(oldValidCode.isExpired()) { throw new AuthenticationException("验证码已过期"); } if(!oldValidCode.valid(validCode)) { throw new AuthenticationException("验证码输入错误"); } //实际需要根据账号,查询当前用户信息 User user = new User(); user.setId(123); user.setName("xs"); user.setPassword("123"); ByteSource salt= ByteSource.Util.bytes(user.getId().toString()); Object password = new SimpleHash("MD5", user.getPassword(), salt, 2); return new SimpleAuthenticationInfo( user, "297254e9bfe0b8f39c682eda30bb9be0", //密码 salt, getName() ); } }
配置UserRealm为Bean
@Bean public UserRealm userRealm() { UserRealm myShiroRealm = new UserRealm(); myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); myShiroRealm.setCachingEnabled(true); //启用身份验证缓存,即缓存AuthenticationInfo信息,默认false myShiroRealm.setAuthenticationCachingEnabled(false); //缓存AuthenticationInfo信息的缓存名称 myShiroRealm.setAuthenticationCacheName("authenticationCache"); //启用授权缓存,即缓存AuthorizationInfo信息,默认false myShiroRealm.setAuthorizationCachingEnabled(true); //缓存AuthorizationInfo信息的缓存名称 myShiroRealm.setAuthorizationCacheName("authorizationCache"); return myShiroRealm; } /** * 密码加密 */ private HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("MD5");//散列算法:这里使用MD5算法; hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 return hashedCredentialsMatcher; }
权限注解
@RequestMapping("/add") @RequiresPermissions("user:add") public String add() { return "user/add"; }
校验码
/** * 校验码 * @author Administrator * */ public class ValidationCode { public final static String VALID_CODE_NAME = "VALIDCODE"; private String code; private Date createTime; private int expireMillisecond = 6000; private ValidationCode(String code) { this.code = code; this.createTime = new Date(); } public static ValidationCode create(String code) { return new ValidationCode(code); } /** * 是否过期 */ public boolean isExpired() { Long between_Millisecond = new Date().getTime()-createTime.getTime(); return between_Millisecond.intValue() > expireMillisecond; } /** * 与客户端code比较是否一致 */ public boolean valid(String newCode) { return this.code.equalsIgnoreCase(newCode); } }
检验码生成类
@Controller @RequestMapping public class ValidationCodeController { @RequestMapping("/validCode") public void captcha(HttpServletRequest request, HttpServletResponse response, @RequestParam(name = "w", defaultValue = "90") Integer width, @RequestParam(name = "h", defaultValue = "38") Integer height, @RequestParam(name = "n", defaultValue = "4") Integer number) throws IOException { ConfigurableCaptchaService configurableCaptchaService = new ConfigurableCaptchaService(); configurableCaptchaService.setColorFactory(new SingleColorFactory(new Color(25, 60, 170))); configurableCaptchaService .setFilterFactory(new CurvesRippleFilterFactory(configurableCaptchaService.getColorFactory())); RandomFontFactory randomFontFactory = new RandomFontFactory(); randomFontFactory.setMinSize(30); randomFontFactory.setMaxSize(30); RandomWordFactory randomWordFactory = new RandomWordFactory(); randomWordFactory.setMinLength(number); randomWordFactory.setMaxLength(number); configurableCaptchaService.setWordFactory(randomWordFactory); configurableCaptchaService.setFontFactory(randomFontFactory); configurableCaptchaService.setHeight(height); configurableCaptchaService.setWidth(width); response.setContentType("image/png"); response.setHeader("Cache-Control", "no-cache, no-store"); response.setHeader("Pragma", "no-cache"); long time = System.currentTimeMillis(); response.setDateHeader("Last-Modified", time); response.setDateHeader("Date", time); response.setDateHeader("Expires", time); // 将VALIDCODE放入Session中 ServletOutputStream stream = null; try { HttpSession session = request.getSession(); stream = response.getOutputStream(); String validate_code = EncoderHelper.getChallangeAndWriteImage(configurableCaptchaService, "png", stream); session.setAttribute(ValidationCode.VALID_CODE_NAME, ValidationCode.create(validate_code)); stream.flush(); } finally { if (stream != null) { stream.close(); } } } }
统一异常处理
/** * 统一异常处理 */ @RestController @ControllerAdvice public class ControllerExceptionHandler { private Logger logger = LoggerFactory.getLogger(getClass()); @ExceptionHandler(DataAccessException.class) public Object handleDuplicateKeyException(DataAccessException e){ logger.error(e.getMessage(), e); return ResultUtil.error("数据库中已存在该记录"); } @ExceptionHandler(AuthorizationException.class) public Object handleAuthorizationException(AuthorizationException e){ logger.error(e.getMessage(), e); return ResultUtil.error("没有权限,请联系管理员授权"); } @ExceptionHandler(Exception.class) public Object handleException(Exception e){ logger.error(e.getMessage(), e); return ResultUtil.error(e.getMessage()); } @ExceptionHandler(IncorrectCredentialsException.class) public Object handleException(IncorrectCredentialsException e){ logger.error(e.getMessage(), e); return ResultUtil.error("用户名或者密码不对"); } @ExceptionHandler(UnknownAccountException.class) public Object handleException(UnknownAccountException e){ logger.error(e.getMessage(), e); return ResultUtil.error("请输入正确的账户"); } }
总结
目前搭建的项目,还没有从数据库获取数据,登录和权限获取的数据目前都是写死的。但是基本架子已经搭建好了,只需要在UserRealm中注入UserService类,提供数据库获取数据的服务即可。还有基于注解权限的方式需要注入LifecycleBeanPostProcessor和DefaultAdvisorAutoProxyCreator,并且DefaultAdvisorAutoProxyCreator.setProxyTargetClass(true)
。
来源:https://www.cnblogs.com/fzsyw/p/11373776.html