概述
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