Spring boot + Spring Security实现权限管理


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

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

任务案例分析

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

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

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

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

需求用例图

权限管理流程

讲义内容

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

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

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

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

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

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

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

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

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

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

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

5.构建成功

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

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

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

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

index.html

user.html

role.html

menu.html

order.html

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

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

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

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

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

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

SpringSecurity基本原理:

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

流程说明:

客户端发起一个请求,进入 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。

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

上图代码分三段理解:

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

2.配置权限

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 \,如下:

重新加载配并重启docker:

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

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

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

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

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

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

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

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

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

@AllArgsConstructor

public 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

@AllArgsConstructor

public 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.启动运行

登录成功

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

理解会话

会话(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。适合于接口型的无状态应用,该方式节省资源。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270