SpringSecurity04 利用JPA和SpringSecurity实现前后端分离的认证和授权
1 环境搭建
1.1 环境说明
JDK:1.8
MAVEN:3.5
SpringBoot:2.0.4
SpringSecurity:5.0.7
IDEA:2017.02旗舰版
1.2 环境搭建
创建一个SpringBoot项目,引入相关依赖:WEB、JPA、MYSQL、SpringSecurity、lombok、devtools
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.xunyji</groupId> <artifactId>springsecurity03</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>springsecurity03</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
1.3 MySQL连接信息配置
在yml配置文件中配置MySQL相关的配置,参考文档
spring: datasource: url: jdbc:mysql://127.0.0.1/testdemo?characterEncoding=utf-8&useSSL=false username: root password: 182838 jpa: properties: hibernate: format_sql: true show_sql: true
1.4 创建一个Restful接口
该接口主要用于测试用的
package com.xunyji.springsecurity03.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author 王杨帅 * @create 2018-09-08 21:51 * @desc **/ @RestController @RequestMapping(value = "/test") @Slf4j public class TestController { @GetMapping(value = "/home") public String home() { String info = "寻渝记主页面"; log.info(info); return info; } }
1.5 启动应用
技巧01:SpringBoot集成了SpringSecurity后,默认会对除了 /login 的所有请求进行拦截认证,所以我们启动应用后访问 /test/home 时会自动重定向到 /login 去进行登录录入
技巧02:SpringSecurity默认提供了一个用户,用户名是 user, 用户密码在启动应用时会打印输出到控制台
技巧03:SpringSecurity默认在登录成功后会自动跳转到之前访问的请求【PS: 这种方式在前后端分离的项目中并不适用,所以需要进行登录成功和失败来接,让登录成功和失败后返回JSON信息给前端,具体怎么跳转有前端进行控制】
2 基于内存的认证和授权
待更新......2018年9月8日22:01:11
3 基于JPA实现SpringSecurity的认证和授权
3.1 创建数据库表
用户表:用于存放用户的相关信息
角色表:用于存放角色信息【PS: 角色表插入数据时必须全部大写,而且要以 ROLE_ 开头】
用户角色表:用户存放用户和角色的关联信息【PS: 用户和角色是多对多的关系】
/* Navicat MySQL Data Transfer Source Server : mysql5.4 Source Server Version : 50540 Source Host : localhost:3306 Source Database : testdemo Target Server Type : MYSQL Target Server Version : 50540 File Encoding : 65001 Date: 2018-09-08 21:30:53 */ SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for `security_authority` -- ---------------------------- DROP TABLE IF EXISTS `security_authority`; CREATE TABLE `security_authority` ( `id` int(11) NOT NULL AUTO_INCREMENT, `authority` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4; -- ---------------------------- -- Records of security_authority -- ---------------------------- INSERT INTO `security_authority` VALUES ('1', 'ROLE_ADMIN'); INSERT INTO `security_authority` VALUES ('2', 'ROLE_USER'); INSERT INTO `security_authority` VALUES ('3', 'ROLE_BOSS'); -- ---------------------------- -- Table structure for `security_user` -- ---------------------------- DROP TABLE IF EXISTS `security_user`; CREATE TABLE `security_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, `email` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4; -- ---------------------------- -- Records of security_user -- ---------------------------- INSERT INTO `security_user` VALUES ('1', 'admin', '$2a$10$AC0nr/qvRHHn2YNsKNbH/.X9f0dHhIZX0457mwFPBJ6jS2/Tcmu3S', '412444321@qq.com'); INSERT INTO `security_user` VALUES ('2', 'wys', '$2a$10$YH9HijmebwcDfTdbx5ho2OlJ6.zewxufvCrnioVGI5PcXFsqNtCd6', '41fasd321@qq.com'); INSERT INTO `security_user` VALUES ('3', 'boss', '$2a$10$iZ/467THEoA7E/MjOA6iJeBpZJpebIfRzvFbZhKNKwyyFfBypmQTi', 'asdfa@qq.com'); -- ---------------------------- -- Table structure for `security_user_role` -- ---------------------------- DROP TABLE IF EXISTS `security_user_role`; CREATE TABLE `security_user_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `role_id` int(11) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4; -- ---------------------------- -- Records of security_user_role -- ---------------------------- INSERT INTO `security_user_role` VALUES ('1', '1', '1'); INSERT INTO `security_user_role` VALUES ('2', '2', '2'); INSERT INTO `security_user_role` VALUES ('3', '3', '1'); INSERT INTO `security_user_role` VALUES ('4', '3', '2'); INSERT INTO `security_user_role` VALUES ('5', '3', '3'); INSERT INTO `security_user_role` VALUES ('6', '1', '2');
3.2 创建实体类
技巧01:利用IDEA批量创建实体类,参考文档
3.4 创建对应的Repository
每个实体类创建一个Repository
技巧01:利用Repository添加用户,添加用户时必须利用PasswordEncoder对密码进行加密
技巧02:添加用户前需要创建一个PasswordEncoder的Bean
3.5 创建服务层
技巧01:本博文只创建了UserService的服务层,因为仅仅是实现认证和授权的功能
技巧02:如果需要实现自定义的认证逻辑,就必须让UserService实现UserDetailsService接口,所有认证相关的逻辑都在loadUserByUsername方法中进行
技巧03:loadUserByUsername方法中主要是根据用户名查找用户信息以及该用户的角色信息,该方法的返回值是SpringSecurity提供的User对象
package com.xunyji.springsecurity02.service; import com.xunyji.springsecurity02.pojo.dataobject.SecurityAuthorityDO; import com.xunyji.springsecurity02.pojo.dataobject.SecurityUserDO; import com.xunyji.springsecurity02.pojo.dataobject.SecurityUserRoleDO; import com.xunyji.springsecurity02.repository.AuthorityRepository; import com.xunyji.springsecurity02.repository.UserRepository; import com.xunyji.springsecurity02.repository.UserRoleRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; /** * @author 王杨帅 * @create 2018-09-08 20:46 * @desc **/ @Service public class UserService implements UserDetailsService { @Autowired private UserRepository userRepository; @Autowired private UserRoleRepository userRoleRepository; @Autowired private AuthorityRepository authorityRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 根据用户名查找用户信息 SecurityUserDO byUsername = userRepository.findByUsername(username); System.out.println(byUsername); // 根据用户ID查找用户角色关联信息 List<SecurityUserRoleDO> byUserId = userRoleRepository.findByUserId(byUsername.getId()); byUserId.stream().forEach(System.out::println); // 获取该用户对应的所有角色的ID列表 List<Integer> collect = byUserId.stream() .map(info -> info.getRoleId()) .collect(Collectors.toList()); collect.stream().forEach(System.out::println); // 获取该用户对应的所有角色信息 List<SecurityAuthorityDO> allById = authorityRepository.findAllById(collect); allById.stream().forEach(System.out::println); List<SimpleGrantedAuthority> authorityList = allById.stream() .map(info -> new SimpleGrantedAuthority(info.getAuthority())) .collect(Collectors.toList()); authorityList.stream().forEach(System.out::println); // 封装该用户的用户名、密码、角色 return new User(byUsername.getUsername(), byUsername.getPassword(), authorityList); } }
3.6 SpringSecurity相关配置
3.6.1 认证成功拦截器
认证成功后默认是跳转到之前的请求API,可以通过认证成功拦截器让认证成功后返回一些JSON信息
package com.xunyji.springsecurity02.config; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author 王杨帅 * @create 2018-09-08 15:19 * @desc **/ @Component @Slf4j public class XiangXuAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { log.info("登录成功"); httpServletResponse.setContentType("application/json;charset=UTF-8"); httpServletResponse.getWriter() .write(objectMapper.writeValueAsString(authentication)); } }
3.6.2 认证失败拦截器
认证失败后默认是跳转到 /login ,可以通过认证失败拦截器让认证失败后返回一些JSON信息
package com.xunyji.springsecurity02.config; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author 王杨帅 * @create 2018-09-08 15:22 * @desc **/ @Component @Slf4j public class XiangXuAuthenticationFailureHandler implements AuthenticationFailureHandler { @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { log.info("登陆失败"); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(exception)); } }
3.6.2 自定义认证授权配置
package com.xunyji.springsecurity02.config; import com.xunyji.springsecurity02.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; /** * @author 王杨帅 * @create 2018-09-08 20:13 * @desc **/ @Configuration public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private PasswordEncoder passwordEncoder; @Autowired private UserService userService; @Autowired private AuthenticationFailureHandler xiangXuAuthenticationFailureHandler; @Autowired private AuthenticationSuccessHandler xiangXuAuthenticationSuccessHandler; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(authenticationProvider()); // 添加自定义的认证逻辑 } @Override public void configure(WebSecurity web) throws Exception { super.configure(web); } @Override protected void configure(HttpSecurity http) throws Exception { http .formLogin() .loginProcessingUrl("/furyLogin") // 提交登录信息的API .usernameParameter("name") // 登录名 .passwordParameter("pwd") // 登录密码 .successHandler(xiangXuAuthenticationSuccessHandler) // 登录成功处理器 .failureHandler(xiangXuAuthenticationFailureHandler) // 登录失败处理器 .and().authorizeRequests() .antMatchers("/test/home").permitAll() // 该api不需要授权 .anyRequest().authenticated() // 剩余都需要授权 .and() .logout().permitAll() // 登出API不需要授权 .and() .csrf().disable(); } /** * 創建PsswordEncoder對應的Bean * @return */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 创建认证提供者Bean * 技巧01:DaoAuthenticationProvider是SpringSecurity提供的AuthenticationProvider实现类 * @return */ @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); // 创建DaoAuthenticationProvider实例 authProvider.setUserDetailsService(userService); // 将自定义的认证逻辑添加到DaoAuthenticationProvider authProvider.setPasswordEncoder(passwordEncoder); // 设置自定义的密码加密 return authProvider; } }
3,7 编写控制类
package com.xunyji.springsecurity02.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author 王杨帅 * @create 2018-09-08 21:17 * @desc **/ @RestController @RequestMapping(value = "/test") @Slf4j @EnableGlobalMethodSecurity(prePostEnabled = true) // 开启授权 public class TestController { @GetMapping(value = "/home") public String home() { String info = "寻渝记主页面"; log.info(info); return info; } @PreAuthorize("hasRole('ROLE_BOSS')") // 拥有ROLE_BOSS角色的用户可以访问 @GetMapping(value = "/boss") public String boss() { String info = "拥有ROLE_BOSS权限的才可以进入"; log.info(info); return info; } @PreAuthorize("hasRole('ROLE_BOSS') OR hasRole('ROLE_ADMIN')") @GetMapping(value = "/admin") public String admin() { String info = "拥有ROLE_ADMIN权限的才可以进入"; log.info(info); return info; } @PreAuthorize("hasRole('ROLE_BOSS') OR hasRole('ROLE_ADMIN') OR hasRole('ROLE_USER')") @GetMapping(value = "/user") public String user() { String info = "拥有ROLE_USER权限的才可以进入"; log.info(info); return info; } }
3.8 启动测试
技巧01:如果没有进行认证前访问需要进行权限的 Restful 服务,就会自动返回一个登录页面【问题:如何返回一个JSON信息呢】
技巧02:登录认证成功后,如果访问该用户权限之外的 Restful 服务,那么就会提示权限不足【即:403状态码】
4 本博文源代码