spring boot 2 + shiro 实现权限管理

Shiro是一个功能强大且易于使用的Java安全框架,主要功能有身份验证、受权、加密和会话管理。
看了网上一些文章,下面2篇文章写得不错。
Springboot2.0 集成shiro权限管理 
Spring Boot:整合Shiro权限框架 html

本身动手敲了下代码,在第一篇文章上加入了第二篇文章的Swagger测试,另外本身加入lombok简化实体类代码,一些地方代码也稍微修改了下,过程当中也碰到一些问题,最终代码成功运行。java

开发版本:
IntelliJ IDEA 2019.2.2
jdk1.8
Spring Boot 2.1.11
MySQL8.0mysql

1、建立SpringBoot项目,添加依赖包和配置application.ymlweb

在IDEA中建立一个新的SpringBoot项目算法

一、pom.xml引用的依赖包以下:spring

<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.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

二、application.ymlsql

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/testdb?useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
  jpa:
    hibernate:
      ddl-auto: update #指定为update,每次启动项目检测表结构有变化的时候会新增字段,表不存在时会新建,若是指定create,则每次启动项目都会清空数据并删除表,再新建
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl #按字段名字建表
        #implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl #驼峰自动映射为下划线格式
    show-sql: true # 默认false,在日志里显示执行的sql语句
    database: mysql
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect

2、建立实体类数据库

建立User、Role、Permission三个实体类,根据规则会自动生成两个中间表,最终数据库有5个表。
另外添加一个model处理登陆结果。apache

一、Userapi

package com.example.shiro.entity; import lombok.Getter; import lombok.Setter; import org.springframework.format.annotation.DateTimeFormat; import javax.persistence.*; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @Entity @Getter @Setter public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long userId; @Column(nullable = false, unique = true) private String userName; //登陆用户名
 @Column(nullable = false) private String name;//名称(昵称或者真实姓名,根据实际状况定义)
 @Column(nullable = false) private String password; private String salt;//加密密码的盐

    private byte state;//用户状态,0:建立未认证(好比没有激活,没有输入验证码等等)--等待验证的用户 , 1:正常状态,2:用户被锁定.
 @ManyToMany(fetch= FetchType.EAGER)//当即从数据库中进行加载数据;
    @JoinTable(name = "UserRole", joinColumns = { @JoinColumn(name = "userId") }, inverseJoinColumns ={@JoinColumn(name = "roleId") }) private List<Role> roleList;// 一个用户具备多个角色
 @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") private LocalDateTime createTime;//建立时间
 @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate expiredDate;//过时日期

    private String email; /**密码盐. 从新对盐从新进行了定义,用户名+salt,这样就不容易被破解 */
    public String getCredentialsSalt(){ return this.userName+this.salt; } }

说明:
这里使用@Getter,@Setter注解,不能使用@Data注解,由于实体使用了jpa的@oneToMany ,加载方式为lazy,在主表查询时关联表未加载,而主表使用@Data后会实现带关联表属性的hashCode和equals等方法。在运行过程当中调用关联表数据时会显示异常 java.lang.stackoverflowerror。

二、Role

package com.example.shiro.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.util.List; @Entity @Getter @Setter public class Role { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long roleId; // 编号
 @Column(nullable = false, unique = true) private String role; // 角色标识程序中判断使用,如"admin",这个是惟一的:

    private String description; // 角色描述,UI界面显示使用

    private Boolean available = Boolean.TRUE; // 是否可用,若是不可用将不会添加给用户 //角色 -- 权限关系:多对多关系;
    @ManyToMany(fetch= FetchType.EAGER) @JoinTable(name="RolePermission",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="permissionId")}) private List<Permission> permissions; // 用户 - 角色关系定义;
 @ManyToMany @JoinTable(name="UserRole",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="userId")}) private List<User> users;// 一个角色对应多个用户
}

三、Permission

package com.example.shiro.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.util.List; @Entity @Getter @Setter public class Permission { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long permissionId;//主键.
 @Column(nullable = false) private String permissionName;//名称.
 @Column(columnDefinition="enum('menu','button')") private String resourceType;//资源类型,[menu|button]

    private String url;//资源路径.

    private String permission; //权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view

    private Long parentId; //父编号

    private String parentIds; //父编号列表

    private Boolean available = Boolean.TRUE; //角色 -- 权限关系:多对多关系;
    @ManyToMany(fetch= FetchType.EAGER) @JoinTable(name="RolePermission",joinColumns={@JoinColumn(name="permissionId")},inverseJoinColumns={@JoinColumn(name="roleId")}) private List<Role> roles; }

四、LoginResult

package com.example.shiro.model; import lombok.Data; @Data public class LoginResult { private boolean isLogin = false; private String result; }

3、DAO

一、添加一个DAO基础接口:BaseRepository

package com.example.shiro.repository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.PagingAndSortingRepository; import java.io.Serializable; @NoRepositoryBean public interface BaseRepository<T, I extends Serializable> extends PagingAndSortingRepository<T, I>, JpaSpecificationExecutor<T> { }

二、UserRepository

package com.example.shiro.repository; import com.example.shiro.entity.User; public interface UserRepository extends BaseRepository<User,Long> { User findByUserName(String userName); }

4、Service

一、LoginService

package com.example.shiro.service; import com.example.shiro.model.LoginResult; public interface LoginService { LoginResult login(String userName, String password); void logout(); }

二、UserService

package com.example.shiro.service; import com.example.shiro.entity.User; public interface UserService { User findByUserName(String userName); }

5、Service.impl

一、LoginServiceImpl

package com.example.shiro.service.impl; import com.example.shiro.model.LoginResult; import com.example.shiro.repository.UserRepository; import com.example.shiro.service.LoginService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Service; @Service public class LoginServiceImpl implements LoginService { @Override public LoginResult login(String userName, String password) { LoginResult loginResult = new LoginResult(); if (userName == null || userName.isEmpty()) { loginResult.setLogin(false); loginResult.setResult("用户名为空"); return loginResult; } String msg = ""; // 一、获取Subject实例对象
        Subject currentUser = SecurityUtils.getSubject(); //        // 二、判断当前用户是否登陆 // if (currentUser.isAuthenticated() == false) { //
// } // 三、将用户名和密码封装到UsernamePasswordToken
        UsernamePasswordToken token = new UsernamePasswordToken(userName, password); // 四、认证
        try { currentUser.login(token);// 传到MyAuthorizingRealm类中的方法进行认证
            Session session = currentUser.getSession(); session.setAttribute("userName", userName); loginResult.setLogin(true); return loginResult; //return "/index";
        } catch (UnknownAccountException e) { e.printStackTrace(); msg = "UnknownAccountException -- > 帐号不存在:"; } catch (IncorrectCredentialsException e) { msg = "IncorrectCredentialsException -- > 密码不正确:"; } catch (AuthenticationException e) { e.printStackTrace(); msg = "用户验证失败"; } loginResult.setLogin(false); loginResult.setResult(msg); return loginResult; } @Override public void logout() { Subject subject = SecurityUtils.getSubject(); subject.logout(); } }

二、UserServiceImpl

package com.example.shiro.service.impl; import com.example.shiro.entity.User; import com.example.shiro.repository.UserRepository; import com.example.shiro.service.UserService; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class UserServiceImpl implements UserService { @Resource private UserRepository userRepository; @Override public User findByUserName(String userName) { return userRepository.findByUserName(userName); } }

6、config配置类

一、建立Realm

package com.example.shiro.config; import com.example.shiro.entity.Permission; import com.example.shiro.entity.Role; import com.example.shiro.entity.User; import com.example.shiro.service.UserService; 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.apache.shiro.util.ByteSource; import javax.annotation.Resource; public class MyShiroRealm extends AuthorizingRealm { @Resource private UserService userService; /** * 身份认证:验证用户输入的帐号和密码是否正确。 * */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //获取用户输入的帐号
        String userName = (String) token.getPrincipal(); //经过username从数据库中查找 User对象. //实际项目中,这里能够根据实际状况作缓存,若是不作,Shiro本身也是有时间间隔机制,2分钟内不会重复执行该方法
        User user = userService.findByUserName(userName); if (user == null) { return null; } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user,//这里传入的是user对象,比对的是用户名,直接传入用户名也没错,可是在受权部分就须要本身从新从数据库里取权限
                user.getPassword(),//密码
                ByteSource.Util.bytes(user.getCredentialsSalt()),//salt=username+salt
                getName()//realm name
 ); return authenticationInfo; } /** * 权限信息 * */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); //若是身份认证的时候没有传入User对象,这里只能取到userName //也就是SimpleAuthenticationInfo构造的时候第一个参数传递须要User对象
        User user  = (User)principals.getPrimaryPrincipal(); for(Role role : user.getRoleList()){ //添加角色
 authorizationInfo.addRole(role.getRole()); for(Permission p:role.getPermissions()){ //添加权限
 authorizationInfo.addStringPermission(p.getPermission()); } } return authorizationInfo; } }

二、配置Shiro

package com.example.shiro.config; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; 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.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; import java.util.HashMap; import java.util.Map; import java.util.Properties; @Configuration public class ShiroConfig { //将本身的验证方式加入容器
 @Bean MyShiroRealm myShiroRealm() { MyShiroRealm myShiroRealm = new MyShiroRealm(); myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myShiroRealm; } //权限管理,配置主要是Realm的管理认证
 @Bean DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(myShiroRealm()); return manager; } //凭证匹配器(密码校验交给Shiro的SimpleAuthenticationInfo进行处理)
 @Bean public HashedCredentialsMatcher hashedCredentialsMatcher(){ HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(2);//散列的次数,好比散列两次,至关于 md5(md5(""));
        return hashedCredentialsMatcher; } // Filter工厂,设置对应的过滤条件和跳转条件
 @Bean ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager()); Map<String, String> filterMap = new HashMap<String, String>(); // 登出
        filterMap.put("/logout", "logout"); // swagger
        filterMap.put("/swagger**/**", "anon"); filterMap.put("/webjars/**", "anon"); filterMap.put("/v2/**", "anon"); // 对全部用户认证
        filterMap.put("/**", "authc"); // 登陆
        bean.setLoginUrl("/login"); // 首页
        bean.setSuccessUrl("/index"); // 未受权页面,认证不经过跳转
        bean.setUnauthorizedUrl("/403"); bean.setFilterChainDefinitionMap(filterMap); return bean; } //开启shiro aop注解支持.
 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager()); return authorizationAttributeSourceAdvisor; } //shiro注解模式下,登陆失败或者是没有权限都是抛出异常,而且默认的没有对异常作处理,配置一个异常处理
    @Bean(name="simpleMappingExceptionResolver") public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() { SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver(); Properties mappings = new Properties(); mappings.setProperty("DatabaseException", "databaseError");//数据库异常处理
        mappings.setProperty("UnauthorizedException","/403"); r.setExceptionMappings(mappings); // None by default
        r.setDefaultErrorView("error");    // No default
        r.setExceptionAttribute("exception");     // Default is "exception"
        return r; } }

三、配置swagger

package com.example.shiro.config; import io.swagger.annotations.ApiOperation; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.any()) .paths(PathSelectors.any()).build(); } private static ApiInfo apiInfo() { return new ApiInfoBuilder() .title("API文档") .description("Swagger API 文档") .version("1.0") .contact(new Contact("name..", "url..", "email..")) .build(); } }

7、controller

一、LoginController用来处理登陆

package com.example.shiro.controller; import com.example.shiro.entity.User; import com.example.shiro.model.LoginResult; import com.example.shiro.service.LoginService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController public class LoginController { @Resource private LoginService loginService; @GetMapping(value = "/login") public String login() { return "登陆页"; } @PostMapping(value = "/login") public String login(@RequestBody User user) { System.out.println("login()"); String userName = user.getUserName(); String password = user.getPassword(); LoginResult loginResult = loginService.login(userName,password); if(loginResult.isLogin()){ return "登陆成功"; } else { return "登陆失败:" + loginResult.getResult(); } } @GetMapping(value = "/index") public String index() { return "主页"; } @GetMapping(value = "/logout") public String logout() { return "退出"; } @GetMapping("/403") public String unauthorizedRole(){ return "没有权限"; } }

二、UserController用来测试访问,权限所有采用注解的方式。

package com.example.shiro.controller; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user") public class UserController { //用户查询
    @GetMapping("/userList") @RequiresPermissions("user:view")//权限管理;
    public String userInfo(){ return "userList"; } //用户添加
    @GetMapping("/userAdd") @RequiresPermissions("user:add")//权限管理;
    public String userInfoAdd(){ return "userAdd"; } //用户删除
    @GetMapping("/userDel") @RequiresPermissions("user:del")//权限管理;
    public String userDel(){ return "userDel"; } }

8、数据库预设一些数据

先运行一遍程序,JPA生成数据库表后,手工执行sql脚本插入样本数据。
用户admin的原始密码是123456。

INSERT INTO `user` (`userId`,`username`,`name`,`password`,`salt`,`state`) VALUES ('1', 'admin', '管理员', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 1); INSERT INTO `permission` (`permissionId`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`) VALUES (1,1,'用户管理',0,'0/','user:view','menu','user/userList'); INSERT INTO `permission` (`permissionId`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`) VALUES (2,1,'用户添加',1,'0/1','user:add','button','user/userAdd'); INSERT INTO `permission` (`permissionId`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`) VALUES (3,1,'用户删除',1,'0/1','user:del','button','user/userDel'); INSERT INTO `role` (`roleid`,`available`,`description`,`role`) VALUES (1,1,'管理员','admin'); INSERT INTO `rolepermission` (`permissionid`,`roleid`) VALUES (1,1); INSERT INTO `rolepermission` (`permissionid`,`roleid`) VALUES (2,1); INSERT INTO `userrole` (`roleid`,`userId`) VALUES (1,1);

9、swagger测试

 一、启动项目,访问http://localhost:8080/swagger-ui.html

  二、访问/user/userAdd, Response body显示登陆页

 三、访问POST的/login,请求参数输入:

{
"userName": "admin",
"password": "123456"
}

 Response body显示登陆成功。

 四、再次访问/user/userAdd,由于登陆成功了而且有权限,此次Response body显示userAdd

 

 五、访问/user/userDel,由于数据库没有配置权限,因此Response body显示没有权限

相关文章
相关标签/搜索