# springboot2-shiro-jpa **Repository Path**: anglabace/springboot2-shiro-jpa ## Basic Information - **Project Name**: springboot2-shiro-jpa - **Description**: SpringBoot2.x /Apache Shiro 1.4.0/Hibernate JPA 实现前后分离的登录权限认证服务 - **Primary Language**: Java - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 4 - **Created**: 2020-03-29 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SpringBoot2 + Shiro + JPA > SpringBoot2.x /Apache Shiro 1.4.0/Hibernate JPA 实现前后分离的登录权限认证服务 SpringBoot 2.x 使用 Spring5, JDK 要求1.8+;部分配置也有对应的调整。请参考项目源码 ## 项目背景 前后端分离的开发方式愈来愈流行,传统的登录认证方式通过过滤器拦截认证,重定向单点登录等方式使得用户体验很差。项目中还有部分渠道登录场景,要求免密码认证登录;而Shiro完全可以很简单的解决这两个问题, 一套接口可以同时满足PC端和APP端登录要求。 ## 解决方案 本项目使用SpringBoot2.0.3和shiro-spring 1.4.0,持久层框架使用Hiberante JPA;session存储使用redis,轻松实现集群会话同步共享。 **适配PC端和APP端登录会话维持** 通过继承Shiro的会话管理器`DefaultWebSessionManager`重写`getSessionId`方法实现,将`sessionId`添加到请求头`Authorization`中适配`PC`传统和`Ajax`以及`APP`会话维持 ```java public class CustomWebSessionManager extends DefaultWebSessionManager { private static final String AUTHORIZATION = "Authorization"; private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request"; public CustomWebSessionManager() { super(); } @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response) { String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION); // 如果请求头中有 Authorization 则其值为sessionId if (!StringUtils.isEmpty(id)) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); return id; } else { // 否则按默认规则从cookie取sessionId return super.getSessionId(request, response); } } } ``` **可靠渠道认证密码登录** 可能有这样一种要求,从某一个平台跳转过来用户免密码直接登录;通过继承凭证匹配器`HashedCredentialsMatcher`重写`doCredentialsMatch`方法实现,自定义免密码`CipherFreeToken`继承自`AuthenticationToken`,直接放行`CipherFreeToken`即可实现免密码登录 ```java public class CipherFreeHashedCredentialsMatcher extends HashedCredentialsMatcher { @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { if (token instanceof CipherFreeToken) { // 免密码登录 return true; } return super.doCredentialsMatch(token, info); } } public class CipherFreeToken extends UsernamePasswordToken { private static final long serialVersionUID = 5478435999847981116L; public CipherFreeToken() { } public CipherFreeToken(String username) { super(username, ""); } public CipherFreeToken(String username, String host) { super(username, "", host); } public CipherFreeToken(String username, boolean rememberMe) { super(username, "", rememberMe); } public CipherFreeToken(String username, boolean rememberMe, String host) { super(username, "", rememberMe, host); } public CipherFreeToken(String username, String password, boolean rememberMe, String host) { super(username, password, rememberMe, host); } } ``` ## Hibernate JPA 使用`spring-data-jpa`封装,`springboot 2.0`对于`jpa`有些改变 - `@GeneratedValue(strategy = GenerationType.IDENTITY)`, 使用`AUTO`时将创建`hibernate_sequence`表 - `findById(ID id)` 返回`Optional` 基类 ```java @MappedSuperclass @Getter @Setter public class JpaEntity { @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) protected ID id; @Column(name = "created_at") protected Long createdAt; @Column(name = "updated_at") protected Long updatedAt; @PrePersist public void prePersist() { this.createdAt = Calendar.getInstance().getTimeInMillis(); } @PreUpdate public void preUpdate() { this.updatedAt = Calendar.getInstance().getTimeInMillis(); } } public interface JpaService> { R getRepository(); default Optional findOne(ID id) { return getRepository().findById(id); } default List findAll() { return getRepository().findAll(); } default T save(T entity) { return getRepository().save(entity); } default List save(List entities) { return getRepository().saveAll(entities); } default void delete(T entity) { getRepository().delete(entity); } default void delete(ID id) { getRepository().deleteById(id); } } @NoRepositoryBean public interface JpaExtraRepository extends JpaRepository { Page findAll(Specification spec, Pageable pageable); } ``` **DDL** ![](assets/scheme.png) ```sql /* SQLEditor (MySQL (2))*/ CREATE TABLE tb_resources ( id INTEGER NOT NULL AUTO_INCREMENT COMMENT '主键', name VARCHAR(64) NOT NULL COMMENT '资源名称', type VARCHAR(20) NOT NULL COMMENT '类型', url VARCHAR(100) COMMENT '资源地址', permission VARCHAR(64) COMMENT '权限修饰符', created_at BIGINT COMMENT '创建时间戳', updated_at BIGINT COMMENT '更新时间戳', PRIMARY KEY (id) ) COMMENT='资源资源表'; CREATE TABLE tb_roles ( id INTEGER NOT NULL AUTO_INCREMENT COMMENT '主键', name VARCHAR(64) COMMENT '角色名称', description VARCHAR(255) COMMENT '描述', resource_ids VARCHAR(255) COMMENT '资源IDs', created_at BIGINT COMMENT '创建时间戳', updated_at BIGINT COMMENT '更新时间戳', PRIMARY KEY (id) ) COMMENT='角色表'; CREATE TABLE tb_users ( id INTEGER NOT NULL AUTO_INCREMENT COMMENT '主键', username VARCHAR(64) NOT NULL UNIQUE COMMENT '用户名', password VARCHAR(64) NOT NULL COMMENT '密码', state VARCHAR(20) COMMENT '状态', created_at BIGINT COMMENT '创建时间戳', updated_at BIGINT COMMENT '更新时间戳', PRIMARY KEY (id) ) COMMENT='用户表'; ``` 实体 ```java @Entity @Getter @Setter @Table(name = "tb_resources") @JsonInclude(JsonInclude.Include.NON_EMPTY) public class Resource extends JpaEntity { @Column(name = "name", length = 64) private String name; @Enumerated(EnumType.STRING) @Column(name = "type", length = 20) private Type type = Type.Menu; @Column(name = "url", length = 255) private String url; @Column(name = "permission", length = 255) private String permission; public enum Type { Menu, Button } } @Entity @Getter @Setter @Table(name = "tb_roles") @JsonInclude(JsonInclude.Include.NON_EMPTY) public class Role extends JpaEntity { @Column(name = "name", length = 64) private String name; @Column(name = "description", length = 255) private String description; /** * 使用资源IDs替代中间表 */ @Column(name = "resource_ids", length = 255) private String resourceIds; } @Entity @Getter @Setter @Table(name = "tb_users") @JsonInclude(JsonInclude.Include.NON_EMPTY) public class User extends JpaEntity { @Column(name = "username", length = 64) private String username; @Column(name = "password", length = 64) private String password; @Column(name = "role_id") private Integer roleId; @Enumerated(EnumType.STRING) @Column(name = "state", length = 20) private State state; // 关联关系,不产生外键 @ManyToOne @JoinColumn(name = "role_id", insertable = false, updatable = false, foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT, name = "none")) private Role role; public enum State { Enabled, Disabled, Deleted } } ``` ## 测试数据 ```java @RunWith(SpringRunner.class) @SpringBootTest public class Springboot2ShiroJpaApplicationTests { @Autowired private ResourceService resourceService; @Autowired private RoleService roleService; @Autowired private UserService userService; @Test public void contextLoads() { } @Test public void testInsertResources() { Resource r1 = new Resource(); r1.setName("系统管理"); r1.setType(Resource.Type.Menu); r1.setUrl("/system"); r1.setPermission("system:*"); Resource r2 = new Resource(); r2.setName("用户管理"); r2.setType(Resource.Type.Menu); r2.setUrl("/users"); r2.setPermission("users:*"); resourceService.save(Arrays.asList(r1, r2)); } @Test public void testInsertRole() { Role r1 = new Role(); r1.setName("ADMIN"); r1.setDescription("管理员"); r1.setResourceIds("1,2"); Role r2 = new Role(); r2.setName("HR"); r2.setDescription("人力资源"); r2.setResourceIds("2"); roleService.save(Arrays.asList(r1, r2)); } @Test public void testInsertUser() { String password = Utils.password("123456"); User u1 = new User(); u1.setUsername("admin"); u1.setPassword(password); u1.setRoleId(1); u1.setState(User.State.Enabled); User u2 = new User(); u2.setUsername("hr"); u2.setPassword(password); u2.setRoleId(2); u1.setState(User.State.Enabled); User u3 = new User(); u3.setUsername("test"); u3.setPassword(password); u3.setState(User.State.Deleted); userService.save(Arrays.asList(u1, u2, u3)); } } ``` ## 控制器 ```java @RestController public class AuthController { @GetMapping("/auth") @RequiresUser public RespResult index(@LoginAccount User user) { return RespResult.builder().result(user).build(); } @GetMapping("/unauth") public RespResult unauth() { RespResult.Builder builder = RespResult.builder(); return builder.code(RespResult.Code.Failure).message("操作未授权").build(); } @RequestMapping("/ok") @RequiresPermissions("system:*") public RespResult ok(@LoginAccount User user) { return RespResult.builder().result(user).build(); } } @RestController @Slf4j public class LoginController { /** * 渠道登录,免密登录 * * @param request HTTP 请求体体 * @return JSON */ @GetMapping(value = "/login") public RespResult login(HttpServletRequest request) { String code = request.getParameter("code"); // 通过code,或其他方式进行渠道认证 Function channel = (r -> "admin"); String login = channel.apply(code); // 执行免密码登录 return doLogin(new CipherFreeToken(login)).build(); } /** * 无状态登录 * * @param login 登录信息 * @return json */ @PostMapping(value = "/login") public RespResult login(Login login) { UsernamePasswordToken token = new UsernamePasswordToken(login.getUsername(), login.getPassword(), login.isRememberMe()); RespResult.Builder resp = doLogin(token); return resp.build(); } /** * 执行登录 * * @param token 令牌 * @return 结果 */ private RespResult.Builder doLogin(AuthenticationToken token) { // 尝试登录 Subject subject = SecurityUtils.getSubject(); RespResult.Builder builder = RespResult.builder(); try { subject.login(token); builder.code(RespResult.Code.Success).data("token", subject.getSession().getId()).message("ok"); } catch (IncorrectCredentialsException e) { builder.code(RespResult.Code.Failure).message("密码错误"); } catch (LockedAccountException e) { builder.code(RespResult.Code.Failure).message("账号锁定,无法登录"); } catch (UnknownAccountException e) { builder.code(RespResult.Code.Failure).message("用户名或密码错误"); } catch (AuthenticationException e) { builder.code(RespResult.Code.Failure).message("认证失败," + e.getMessage()); } catch (Exception e) { builder.code(RespResult.Code.Exception).message(e.getMessage()); } return builder; } } ``` ## 演示效果 渠道登录 ![](assets/login_admin.png) 密码登录 ![](assets/login_hr.png) 有权限 ![](assets/admin_ok.png) 无权限 ![](assets/hr_ok.png)