SpringSecurity+JWT权限管理实战训练营(实现权限管理)

qinzhiqiang 07-18 11:30 792次浏览

SpringSecurity+JWT权限管理实战训练营(实现权限管理)

基于Spring boot + Spring Security实现第一版传统架构

本文是实训邦的权限管理SpringSecurity+JWT的一个讲义,分享给粉丝学习。想要对应学习视频可以联系哈。

任务案例分析

权限管理是一个几乎所有后台系统的都会涉及的一个重要组成部分,可以说是后台项目的基本功,主要目的是对整个后台管理系统进行权限的控制,而针对的对象是员工,避免因权限控制缺失或操作不当引发的风险问题,如操作错误,数据泄露等问题。

权限管理主要是管控下面三个方面:

哪些页面要设置权限哪些操作要设置权限哪些数据要设置权限下面的例子就是控制页面的访问权限:

权限管理核心就是两方面:认证和授权。下面我们参考一下认证的演进过程,去深入了解一下:

需求用例图

SpringSecurity+JWT权限管理实战训练营之实现权限管理

权限管理流程

SpringSecurity+JWT权限管理实战训练营之实现权限管理

讲义内容

1.使用Spring Security的HttpBasic模式实现登录认证

2.使用Spring Security的FormLogin模式实现登录认证

3.基于JSON的前后端分离开发的登录认证

4.将权限管理系统部署到阿里云的docker;

5.基于MySQL数据库的认证和授权。

1使用SpringSecurity的HttpBasic模式实现登录认证

SpringSecurity+JWT权限管理实战训练营之实现权限管理

1.使用Spring Initializr快速构建项目

具体步骤: 在Intellij IDEA中选择 File -> New – > Project -> Spring Initializr -> 点击Next 。

SpringSecurity+JWT权限管理实战训练营之实现权限管理

2.填写Group,Artifact,Packing选择Jar ,点击Next。

SpringSecurity+JWT权限管理实战训练营之实现权限管理

3.选取依赖,这里我们选择Developer Tools中的Lombok、 Web依赖中的Spring Web、Templates Engines中的Thymeleaf 以及Security中的Spring Security,点击Next

SpringSecurity+JWT权限管理实战训练营之实现权限管理

4.核对Project name和Project location,默认不变,选择Finish。

SpringSecurity+JWT权限管理实战训练营之实现权限管理

5.构建成功

SpringSecurity+JWT权限管理实战训练营之实现权限管理

2.使用Thymeleaf制作项目业务页面

具体步骤: 在com.sxbang.fridaysecuritytask包上右键选择 New – > Java Class,输入名字‘controller.HomeController’,点击回车确定。

SpringSecurity+JWT权限管理实战训练营之实现权限管理
SpringSecurity+JWT权限管理实战训练营之实现权限管理

2.根据业务需求编辑HomeController.java。

SpringSecurity+JWT权限管理实战训练营之实现权限管理

3.使用Thymeleaf制作下面页面:

index.html

SpringSecurity+JWT权限管理实战训练营之实现权限管理

user.html

SpringSecurity+JWT权限管理实战训练营之实现权限管理

role.html

SpringSecurity+JWT权限管理实战训练营之实现权限管理

menu.html

SpringSecurity+JWT权限管理实战训练营之实现权限管理

order.html

SpringSecurity+JWT权限管理实战训练营之实现权限管理

3.启动运行项目,实现Httpbasic模式的登录认证

启动运行项目之后,我们可以看到无需任何配置就实现了登录认证功能,这个就是SpringSecurity的Httpbasic模式。

1.启动运行项目后,可以在控制台看到输出的密码,我们首先复制这个密码:

SpringSecurity+JWT权限管理实战训练营之实现权限管理

2.使用浏览器访问localhost:8080:

会弹出一个登录框,这个登录框不是我们编码实现的,是由SpringSecurity来实现。

SpringSecurity+JWT权限管理实战训练营之实现权限管理

3.在登陆页面的Username中输入user,在Password中把刚刚在控制台复制的密码粘贴进去,点击Signin,就可以成功访问到我们的页面啦。

SpringSecurity+JWT权限管理实战训练营之实现权限管理

SpringSecurity基本原理:

SpringSecurity+JWT权限管理实战训练营之实现权限管理

其中,表单登录只是其中的一种过滤方式,httpBasic这种过滤方式是在表单登录之后,类似于责任链模式,除了这两种方式SpringSecurity还支持很多种过滤方式。当请求通过这些绿色的过滤器之后,请求会进入到FilterSecurityInterceptor适配器上,这个是整个SpringSecurity过滤器的最后一环,是最终的守门人,它会去决定请求最终能否去访问到我们的Rest服务。

SpringSecurity+JWT权限管理实战训练营之实现权限管理

流程说明:客户端发起一个请求,进入 Security 过滤器链。当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。

2.使用Spring Security的FormLogin模式实现登录认证

相信大家看过上面HttpBasic模式后发现实际项目应用中它并不适合,因为我们往往都是自己开发一个自定义的登陆页面,Spring Security的FormLogin模式就支持这种需求,下面我们使用FormLogin模式来改写我们的登录认证。

我们先来一起看下需求:1.应用中的所有请求都需要用户登录之后才能访问2.我们需要自己开发一个登陆页面(login.html)3.我们要允许所有用户有权访问登录页4.如果用户没有登陆,必须跳转到登录页进行登录5.用户成功登陆后,我们需要根据用户不同的角色进行授权。

下面我们开始使用FormLogin模式,具体步骤:

1.编写login.html。

SpringSecurity+JWT权限管理实战训练营之实现权限管理

2.创建一个继承WebSecurityConfigurerAdapter的SecurityConfig类,重写configure(HttpSecurity http) 方法,用来配置登录验证逻辑。

SpringSecurity+JWT权限管理实战训练营之实现权限管理

上图代码分三段理解:

1.配置认证,开启formLogin模式

2.配置权限

3.禁用跨站csrf攻击防御。

SpringSecurity+JWT权限管理实战训练营之实现权限管理
3.这里我们采用内存中身份认证的方法,在SecurityConfig类重写configure(AuthenticationManagerBuilder auth)方法,增加user和admin两个用户的配置,后续我们会根据RBAC模型设计数据表,实现基于数据库动态的配置。

官方推荐使用BCryptPasswordEncoder进行密码加密。

bcrypt是一种跨平台的文件加密工具。bcrypt 使用的是布鲁斯·施内尔在1993年发布的 Blowfish 加密算法。由它加密的文件可在所有支持的操作系统和处理器上进行转移。它的口令必须是8至56个字符,并将在内部被转化为448位的密钥。

SpringSecurity+JWT权限管理实战训练营之实现权限管理

4.运行验证,使用浏览器访问:localhost:8080。

SpringSecurity+JWT权限管理实战训练营之实现权限管理
SpringSecurity+JWT权限管理实战训练营之实现权限管理
SpringSecurity+JWT权限管理实战训练营之实现权限管理

根据权限配置,user用户可以访问订单页面,不能访问用户管理、角色管理和菜单管理,下面我们分别访问订单页面和用户管理页面,看一下是不是和我们的代码配置一致。

点击订单页面,可以正常访问:

SpringSecurity+JWT权限管理实战训练营之实现权限管理

点击用户管理页面,提示我们被禁止访问:

SpringSecurity+JWT权限管理实战训练营之实现权限管理

5.一行代码实现登出功能。

Spring Security帮我们实现登出功能的大部分代码,我们只需要在configure(HttpSecurity httpSecurity)方法内添加一行即可:

SpringSecurity+JWT权限管理实战训练营之实现权限管理

然后在index.html中增加‘<a href=”/logout” >退出</a>’即可。

SpringSecurity+JWT权限管理实战训练营之实现权限管理

3,基于JSON的前后端分离开发的登录认证

前面的例子,在发送登录请求并认证成功之后,页面会跳转回原访问页,但在前后端分离开发、通过JSON数据完成交互的应用中,会在登录时返回一段JSON数据,告知前端登录成功与否,由前端决定如何处理后续逻辑,而非由服务器主动执行页面跳转,下面我们就看看这种情况如何实现。

Spring Security表单登录配置模块提供了successHandler()和failureHandler()两个方法,分别处理登录成功和登录失败的逻辑。其中,successHandler()方法带有一个Authentication参数,携带当前登录用户名及其角色等信息;而failureHandler()方法携带一个AuthenticationException异常参数。具体处理方式需按照系统的情况自定义。

实现思路:1.修改login.html,使用JSON数据传递username和password;2.自定义登陆成功时的处理逻辑;3.自定义登录失败时的处理逻辑。

实现步骤:1.修改login.html

SpringSecurity+JWT权限管理实战训练营之实现权限管理

2.在com.sxbang.fridaysecuritytask.config.security.handler包下创建MyAuthenticationSuccessHandler类,使它实现AuthenticationSuccessHandler接口,重写onAuthenticationSuccess(HttpServletRequesthttpServletRequest,HttpServletResponsehttpServletResponse,Authenticationauthentication)方法来实现登陆成功返回逻辑。

SpringSecurity+JWT权限管理实战训练营之实现权限管理

3.在com.sxbang.fridaysecuritytask.config.security.handler包下创建MyAuthenticationFailureHandler类,使它实现AuthenticationFailureHandler接口,重写onAuthenticationFailure(HttpServletRequesthttpServletRequest,HttpServletResponsehttpServletResponse,AuthenticationExceptione)方法来实现登陆失败返回逻辑。

SpringSecurity+JWT权限管理实战训练营之实现权限管理

4.在configure(HttpSecurityhttpSecurity)方法中分别调用successHandler(successHandler)和failureHandler(failureHandler)方法来实现自定义处理逻辑

SpringSecurity+JWT权限管理实战训练营之实现权限管理

5.运行检验效果

4,将权限管理系统部署到阿里云的docker

本节内容,我们使用IntelliJ IDEA的Docker插件帮助我们将当前权限管理应用制作成Docker镜像、运行在指定的远程机器(阿里云)上。

实现步骤:1.在CentSO系统上开启Docker的远程连接,如果你对docker安装和基本的操作还不熟悉,请参考我的docker课程之后再继续下面的内容。

编辑此文件:/lib/systemd/system/docker.service,把ExecStart=/usr/bin/dockerd-current \改为ExecStart=/usr/bin/dockerd-current -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock \,如下:

SpringSecurity+JWT权限管理实战训练营之实现权限管理

重新加载配并重启docker:

SpringSecurity+JWT权限管理实战训练营之实现权限管理

确保端口2375已开启,如果使用阿里云等云服务,记得在安全策略上配置端口2375

2.IntellijIDEA安装Docker插件,打开Idea,从File->Settings->Plugins->InstallJetBrainsplugin进入插件安装界面,在搜索框中输入docker,可以看到Dockerintegration,点击右边的Install按钮进行安装。安装后重启Idea。

SpringSecurity+JWT权限管理实战训练营之实现权限管理

3.重启后配置docker,连接到远程docker服务。从File->Settings->Build,Execution,Deployment->Docker打开配置界面。在设置页面,按照下图的数字顺序创建一个Dockerserver并进行设置,输入Docker服务所在机器的IP地址,如果连接成功页面上会立即提示”Connectionsuccessful”

SpringSecurity+JWT权限管理实战训练营之实现权限管理

4.在fridaysecuritytask项目目录下创建Dockerfile,内容如下:

SpringSecurity+JWT权限管理实战训练营之实现权限管理

5.按照下图操作,创建一个Dockerfile的配置

在个”RunMavenGoal”点击后,输入要执行的maven命令cleanpackage-U-DskipTests,表示每次在构建镜像之前,都会将当前工程清理掉并且重新编译构建:

SpringSecurity+JWT权限管理实战训练营之实现权限管理
SpringSecurity+JWT权限管理实战训练营之实现权限管理
SpringSecurity+JWT权限管理实战训练营之实现权限管理

6.点击三角按钮运行验证

SpringSecurity+JWT权限管理实战训练营之实现权限管理

启动运行成功,使用浏览器访问:http://宿主机IP:8081,如果是阿里云等云服务,记得在安全组规则中增加8081端口.

SpringSecurity+JWT权限管理实战训练营之实现权限管理
SpringSecurity+JWT权限管理实战训练营之实现权限管理
SpringSecurity+JWT权限管理实战训练营之实现权限管理

5.基于MySQL数据库的认证和授权

到目前为止,我们仍然只有一个可登录的用户,怎样引入多用户呢?非常简单,我们只需实现一个自定义的UserDetailsService即可。

UserDetailsService仅定义了一个loadUserByUsername方法,用于获取一个UserDetails对象。UserDetails对象包含了一系列在验证时会用到的信息,包括用户名、密码、权限以及其他信息,Spring Security会根据这些信息判定验证是否成功。

1.创建我们的数据表,并插入三条数据,这里要注意,对密码123456我们使用的机密存储。

-- ------------------------------ Table structure for users-- ----------------------------DROP TABLE IF EXISTS `users`;CREATE TABLE `users` (`user_id` bigint NOT NULL AUTO_INCREMENT,`user_name` varchar(30) COLLATE utf8mb4_general_ci NOT NULL,  `password` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL, `status` char(1) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '0正常1停用',`roles` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '多个角色用逗号间隔',  PRIMARY KEY (`user_id`)) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;-- ------------------------------ Records of users-- ----------------------------INSERT INTO `users` VALUES ('1', 'admin', '$2a$10$nNQI9Ij1rU5NG9JFLQphweTOteCX6O211Nysrg2V5rRSGDRmRWtm.', '0', 'ROLE_ADMIN,ROLE_USER');INSERT INTO `users` VALUES ('2', 'user', '$2a$10$nNQI9Ij1rU5NG9JFLQphweTOteCX6O211Nysrg2V5rRSGDRmRWtm.', '0', 'ROLE_USER');INSERT INTO `users` VALUES ('3', 'alex', '$2a$10$nNQI9Ij1rU5NG9JFLQphweTOteCX6O211Nysrg2V5rRSGDRmRWtm.', '0', 'ROLE_ADMIN,ROLE_USER');

2.在pom.xml中配置MySQL数据库以及Spring data jpa

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

3.在application.yml中配置数据库来连接参数

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:12345/friday?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: 123456

4.构建Users实体

@Entity@Table(name = "users")@Data@NoArgsConstructor@AllArgsConstructorpublic class Users {    /**     * 用户ID     */    @Id    @GeneratedValue(strategy = GenerationType.IDENTITY)    @Column    private Long userId;    /**     * 用户账号     */    @Column(name = "user_name")    private String userName;    /**     * 密码     */    @Column(name = "password")    private String password;    /**     * 帐号状态(0正常 1停用)     */    @Column(name = "status")    private String status;    /**     * 用户角色(多角色用逗号间隔)     */    @Column(name = "roles")    private String roles;}

5.Users实体实现UserDetails接口,实现UserDetails定义的几个方法: ◎ isAccountNonExpired、isAccountNonLocked 和 isCredentialsNonExpired 暂且用不到,统一返回 true,否则Spring Security会认为账号异常。 ◎ isEnabled对应enable字段,将其代入即可。 ◎ getAuthorities方法本身对应的是roles字段,但由于结构不一致,所以此处新建一个,并在后续进行填充。

/** * 用户对象 users */@Entity@Table(name = "users")@Data@NoArgsConstructor@AllArgsConstructorpublic class Users implements UserDetails {    /**     * 用户ID     */    @Id    @GeneratedValue(strategy = GenerationType.IDENTITY)    @Column    private Long userId;    /**     * 用户账号     */    @Column(name = "user_name")    private String userName;    /**     * 密码     */    @Column(name = "password")    private String password;    /**     * 帐号状态(0正常 1停用)     */    @Column(name = "status")    private String status;    /**     * 用户角色(多角色用逗号间隔)     */    @Column(name = "roles")    private String roles;    //实体类中使想要添加表中不存在字段,就要使用@Transient这个注解了。    @Transient    private List<GrantedAuthority> authorities;    public void setAuthorities(List<GrantedAuthority> authorities) {        this.authorities = authorities;    }    @Override    public Collection<? extends GrantedAuthority> getAuthorities() {        return this.authorities;    }    @Override    public String getUsername() {        return this.userName;    }    /**     * 账户是否未过期,过期无法验证     */    @Override    public boolean isAccountNonExpired() {        return true;    }    /**     * 指定用户是否解锁,锁定的用户无法进行身份验证     *     * @return     */    @Override    public boolean isAccountNonLocked() {        return true;    }    /**     * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证     *     * @return     */    @Override    public boolean isCredentialsNonExpired() {        return true;    }    /**     * 是否可用 ,禁用的用户不能身份验证     *     * @return     */    @Override    public boolean isEnabled() {        return true;    }

6.编写接口UserDAO,继承JpaRepository<T, ID>,

@Repository public interface UserDAO extends JpaRepository<Users, Long> { }

7.我们需要根据输入用户名在数据库中查询User数据,然后和输入的密码作比较,现在我们来实现根据用户名查找User数据的代码,新增UserService以及它的实现类。

public interface UserService {   public Users selectUserByUserName(String userName); } @Service public class UserServiceImpl implements UserService {   @Autowired  private UserDAO userDAO;  @Override  public Users selectUserByUserName(String userName) {     Users user = new Users(); user.setUserName(userName);    List<Users> list = userDAO.findAll(Example.of(user));    return list.isEmpty() ? null : list.get(0);   } }

8.实现UserDetailService逻辑

@Service public class UserDetailServiceImpl implements UserDetailsService {   @Autowired   private UserService userService;   @Override   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {     Users users = userService.selectUserByUserName(username);     if (users == null){       throw new UsernameNotFoundException("登录用户:" + username + " 不存在");     }     //将数据库的roles解析为UserDetails的权限集     //AuthorityUtils.commaSeparatedStringToAuthorityList将逗号分隔的字符集转成权限对象列表    users.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(users.getRoles()));    return users;   } }

9.修改SecurityConfig文件,将之前的内容认证方式注销掉,使用UserDetailService逻辑来实现登录逻辑认证。

@Override public void configure(AuthenticationManagerBuilder auth) throws Exception {   auth.userDetailsService(myUserDetailsService).passwordEncoder(bCryptPasswordEncoder());} // @Override// protected void configure(AuthenticationManagerBuilder auth) throws Exception {// auth.inMemoryAuthentication()// .withUser("user")// .password(bCryptPasswordEncoder().encode("123456")) // .roles("USER") // .and() // .withUser("admin")// .password(bCryptPasswordEncoder().encode("123456"))// .roles("ADMIN") // .and()// .passwordEncoder(bCryptPasswordEncoder());//配置BCrypt加密// }

10.启动运行

SpringSecurity+JWT权限管理实战训练营之实现权限管理

登录成功

SpringSecurity+JWT权限管理实战训练营之实现权限管理

Spring Security提供4种方式精确的控制会话的创建:

理解会话

SpringSecurity+JWT权限管理实战训练营之实现权限管理
SpringSecurity+JWT权限管理实战训练营之实现权限管理

会话(session)就是无状态的 HTTP 实现用户状态可维持的一种解决方案。HTTP 本身的无状态使得用户在与服务器的交互过程中,每个请求之间都没有关联性。这意味着用户的访问没有身份记录,站点也无法为用户提供个性化的服务。session的诞生解决了这个难题,服务器通过与用户约定每个请求都携带一个id类的信息,从而让不同请求之间有了关联,而id又可以很方便地绑定具体用户,所以我们可以把不同请求归类到同一用户。基于这个方案,为了让用户每个请求都携带同一个id,在不妨碍体验的情况下,cookie是很好的载体。当用户首次访问系统时,系统会为该用户生成一个sessionId,并添加到cookie中。在该用户的会话期内,每个请求都自动携带该cookie,因此系统可以很轻易地识别出这是来自哪个用户的请求。

4种方式控制会话

always:如果当前请求没有session存在,Spring Security创建一个session;ifRequired(默认): Spring Security在需要时才创建;sessionnever: Spring Security将永远不会主动创建session,但是如果session已经存在,它将使用该session;stateless:Spring Security不会创建或使用任何session。适合于接口型的无状态应用,该方式节省资源。

  • 暂无推荐