基于springsecurity整合springboot实现简单认证受权进行修改实现分布式认证,即咱们常说的单点登陆,简称SSO
,指的是在多应用系统的项目中,用户只须要登陆一次,就能够访问全部互相信任的应用系统。html
1. 单点登陆
总结一下,单点登陆的实现分两大环节: 用户认证:这一环节主要是用户向认证服务器发起认证请求,认证服务器给用户返回一个成功的令牌 token
, 主要在认证服务器中完成,即图中的A系统,注意A系统只能有一个。 身份校验:这一环节是用户携带 token
去访问其余服务器时,在其余服务器中要对token
的真伪进行检验,主 要在资源服务器中完成,即图中的B系统,这里B系统能够有不少个。java
2. jwt和rsa
1. JWT 介绍
从分布式认证流程中,咱们不难发现,这中间起最关键做用的就是token
,token
的安全与否,直接关系到系统的健壮性,这里咱们选择使用JWT
来实现token
的生成和校验。 JWT
,全称JSON Web Token
,官网地址https://jwt.io ,是一款出色的分布式身份校验方案。能够生成token
,也能够解析检验token
。 JWT
生成的token
由三部分组成:mysql
-
头部:主要设置一些规范信息,签名部分的编码格式就在头部中声明。git
-
载荷:
token
中存放有效信息的部分,好比用户名,用户角色,过时时间等,可是不要放密码,会泄露!程序员 -
签名:将头部与载荷分别采用
base64
编码后,用“.”相连,再加入盐,最后使用头部声明的编码类型进行编码,就获得了签名。web
JWT生成token的安全性分析 从JWT
生成的token
组成上来看,要想避免token
被伪造,主要就得看签名部分了,而签名部分又有三部分组成,其中头部和载荷的base64
编码,几乎是透明的,毫无安全性可言,那么最终守护token安全的重担就落在了加入的盐上面了! 试想:若是生成token
所用的盐与解析token
时加入的盐是同样的。岂不是相似于中国人民银行把人民币防伪技术公开了?你们能够用这个盐来解析token
,就能用来伪造token
。 这时,咱们就须要对盐采用非对称加密的方式进行加密,以达到生成token
与校验token
方所用的盐不一致的安全效果!算法
2. 非对称加密 RSA
介绍
- 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥能够下发给信任客户端
- 私钥加密,持有私钥或公钥才能够解密
- 公钥加密,持有私钥才可解密
- 优势:安全,难以破解
- 缺点:算法比较耗时,为了安全,能够接受
3. 建立项目
1. 建立父工程
pom.xml
以下spring
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <packaging>pom</packaging> <modules> <module>common</module> <module>server_auth</module> <module>server_resource</module> </modules> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>springboot_security_jwt_rsa_parent</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot_security_jwt_rsa_parent</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> </project>
2. 建立工具模块
pom.xml
以下sql
<?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"> <parent> <artifactId>springboot_security_jwt_rsa_parent</artifactId> <groupId>com.example</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>common</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.10.7</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.10.7</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.10.7</version> <scope>runtime</scope> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-logging --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.10.1</version> </dependency> <!-- https://mvnrepository.com/artifact/joda-time/joda-time --> <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.10.5</version> </dependency> <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> </dependencies> </project>
1. JsonUtils
package com.example.utils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.List; import java.util.Map; /** * @author: 黑马程序员 **/ public class JsonUtils { public static final ObjectMapper mapper = new ObjectMapper(); private static final Logger logger = LoggerFactory.getLogger(JsonUtils.class); //json化 public static String toString(Object obj) { if (obj == null) { return null; } if (obj.getClass() == String.class) { return (String) obj; } try { return mapper.writeValueAsString(obj); } catch (JsonProcessingException e) { logger.error("json序列化出错:" + obj, e); return null; } } //json解析 public static <T> T toBean(String json, Class<T> tClass) { try { return mapper.readValue(json, tClass); } catch (IOException e) { logger.error("json解析出错:" + json, e); return null; } } //解析list的json数据 public static <E> List<E> toList(String json, Class<E> eClass) { try { return mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(List.class, eClass)); } catch (IOException e) { logger.error("json解析出错:" + json, e); return null; } } //json转map public static <K, V> Map<K, V> toMap(String json, Class<K> kClass, Class<V> vClass) { try { return mapper.readValue(json, mapper.getTypeFactory().constructMapType(Map.class, kClass, vClass)); } catch (IOException e) { logger.error("json解析出错:" + json, e); return null; } } //json解析自定义类型 public static <T> T nativeRead(String json, TypeReference<T> type) { try { return mapper.readValue(json, type); } catch (IOException e) { logger.error("json解析出错:" + json, e); return null; } } }
2. JwtUtils
package com.example.utils; import com.example.domain.Payload; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.joda.time.DateTime; import java.security.PrivateKey; import java.security.PublicKey; import java.util.Base64; import java.util.UUID; /** * @author: 黑马程序员 * 生成token以及校验token相关方法 */ public class JwtUtils { private static final String JWT_PAYLOAD_USER_KEY = "user"; /** * 私钥加密token * * @param userInfo 载荷中的数据 * @param privateKey 私钥 * @param expire 过时时间,单位分钟 * @return JWT */ public static String generateTokenExpireInMinutes(Object userInfo, PrivateKey privateKey, int expire) { return Jwts.builder() .claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo)) .setId(createJTI()) .setExpiration(DateTime.now().plusMinutes(expire).toDate()) .signWith(privateKey, SignatureAlgorithm.RS256) .compact(); } /** * 私钥加密token * * @param userInfo 载荷中的数据 * @param privateKey 私钥 * @param expire 过时时间,单位秒 * @return JWT */ public static String generateTokenExpireInSeconds(Object userInfo, PrivateKey privateKey, int expire) { return Jwts.builder() .claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo)) .setId(createJTI()) .setExpiration(DateTime.now().plusSeconds(expire).toDate()) .signWith(privateKey, SignatureAlgorithm.RS256) .compact(); } /** * 公钥解析token * * @param token 用户请求中的token * @param publicKey 公钥 * @return Jws<Claims> */ private static Jws<Claims> parserToken(String token, PublicKey publicKey) { return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token); } private static String createJTI() { return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes())); } /** * 获取token中的用户信息 * * @param token 用户请求中的令牌 * @param publicKey 公钥 * @return 用户信息 */ public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey, Class<T> userType) { Jws<Claims> claimsJws = parserToken(token, publicKey); Claims body = claimsJws.getBody(); Payload<T> claims = new Payload<>(); claims.setId(body.getId()); claims.setUserInfo(JsonUtils.toBean(body.get(JWT_PAYLOAD_USER_KEY).toString(), userType)); claims.setExpiration(body.getExpiration()); return claims; } /** * 获取token中的载荷信息 * * @param token 用户请求中的令牌 * @param publicKey 公钥 * @return 用户信息 */ public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey) { Jws<Claims> claimsJws = parserToken(token, publicKey); Claims body = claimsJws.getBody(); Payload<T> claims = new Payload<>(); claims.setId(body.getId()); claims.setExpiration(body.getExpiration()); return claims; } }
3. RsaUtils
package com.example.utils; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.security.*; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; /** * @author 黑马程序员 */ public class RsaUtils { private static final int DEFAULT_KEY_SIZE = 2048; /** * 从文件中读取公钥 * * @param filename 公钥保存路径,相对于classpath * @return 公钥对象 * @throws Exception */ public static PublicKey getPublicKey(String filename) throws Exception { byte[] bytes = readFile(filename); return getPublicKey(bytes); } /** * 从文件中读取密钥 * * @param filename 私钥保存路径,相对于classpath * @return 私钥对象 * @throws Exception */ public static PrivateKey getPrivateKey(String filename) throws Exception { byte[] bytes = readFile(filename); return getPrivateKey(bytes); } /** * 获取公钥 * * @param bytes 公钥的字节形式 * @return * @throws Exception */ private static PublicKey getPublicKey(byte[] bytes) throws Exception { bytes = Base64.getDecoder().decode(bytes); X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes); KeyFactory factory = KeyFactory.getInstance("RSA"); return factory.generatePublic(spec); } /** * 获取密钥 * * @param bytes 私钥的字节形式 * @return * @throws Exception */ private static PrivateKey getPrivateKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException { bytes = Base64.getDecoder().decode(bytes); PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes); KeyFactory factory = KeyFactory.getInstance("RSA"); return factory.generatePrivate(spec); } /** * 根据密文,生存rsa公钥和私钥,并写入指定文件 * * @param publicKeyFilename 公钥文件路径 * @param privateKeyFilename 私钥文件路径 * @param secret 生成密钥的密文 */ public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret, int keySize) throws Exception { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); SecureRandom secureRandom = new SecureRandom(secret.getBytes()); keyPairGenerator.initialize(Math.max(keySize, DEFAULT_KEY_SIZE), secureRandom); KeyPair keyPair = keyPairGenerator.genKeyPair(); // 获取公钥并写出 byte[] publicKeyBytes = keyPair.getPublic().getEncoded(); publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes); writeFile(publicKeyFilename, publicKeyBytes); // 获取私钥并写出 byte[] privateKeyBytes = keyPair.getPrivate().getEncoded(); privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes); writeFile(privateKeyFilename, privateKeyBytes); } private static byte[] readFile(String fileName) throws Exception { return Files.readAllBytes(new File(fileName).toPath()); } private static void writeFile(String destPath, byte[] bytes) throws IOException { File dest = new File(destPath); if (!dest.exists()) { dest.createNewFile(); } Files.write(dest.toPath(), bytes); } }
4. pojo类Payload
package com.example.domain; import lombok.Data; import java.util.Date; /** * @author john * @date 2020/1/12 - 9:15 */ @Data public class Payload<T> { private String id; private T userInfo; private Date expiration; }
5. 测试生成rsa签名的公钥和私钥
package com.example.utils; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; /** * @author john * @date 2020/1/12 - 9:35 */ class RsaUtilsTest { private String privateFilePath = "D:\\imooc\\study\\springboot_security_jwt_rsa_parent\\key\\private.key"; private String publicFilePath = "D:\\imooc\\study\\springboot_security_jwt_rsa_parent\\key\\rsa_public.pub"; @Test void getPublicKey() throws Exception { System.out.println(RsaUtils.getPublicKey(publicFilePath)); } @Test void getPrivateKey() throws Exception { System.out.println(RsaUtils.getPrivateKey(privateFilePath)); } @Test void generateKey() throws Exception { RsaUtils.generateKey(publicFilePath, privateFilePath, "saltss", 2048); } }
效果如图所示apache
3. 建立认证模块
pom.xml
以下
<?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"> <parent> <artifactId>springboot_security_jwt_rsa_parent</artifactId> <groupId>com.example</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>server_auth</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</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-test</artifactId> </dependency> <dependency> <groupId>com.example</groupId> <artifactId>common</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.48</version> </dependency> <!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> <scope>provided</scope> </dependency> </dependencies> </project>
1. application.yml
server: port: 8081 spring: datasource: driver-class-name: com.mysql.jdbc.Driver password: root username: root url: jdbc:mysql:///test mybatis: type-aliases-package: com.example.domain configuration: map-underscore-to-camel-case: true logging: level: com.example: debug rsa: key: privateKeyFile: D:\test\auth\id_rsa_private_pkcs publicKeyFile: D:\test\auth\id_rsa_public.pub
2. 提供解析公钥和私钥的配置类
@ConfigurationProperties(prefix = "rsa.key") @Data public class RsaKeyProperties { private String publicKeyFile; private String privateKeyFile; private PublicKey publicKey; private PrivateKey privateKey; @PostConstruct public void createRsaKey() throws Exception { publicKey = RsaUtils.getPublicKey(publicKeyFile); privateKey = RsaUtils.getPrivateKey(privateKeyFile); } }
3. 建立认证服务启动类
@SpringBootApplication @MapperScan("com.example.mapper") @EnableConfigurationProperties(RsaKeyProperties.class) public class AuthServerApplication { public static void main(String[] args) { SpringApplication.run(AuthServerApplication.class, args); } }
4. 编写认证过滤器
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter { private AuthenticationManager authenticationManager; private RsaKeyProperties prop; public JwtLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) { this.authenticationManager = authenticationManager; this.prop = prop; } //重写springsecurity获取用户名和密码操做 public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { //从输入流中获取用户名和密码,而不是表单 SysUser sysUser = new ObjectMapper().readValue(request.getInputStream(), SysUser.class); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword()); return authenticationManager.authenticate(authRequest); } catch (Exception e) { try { //处理失败请求 response.setContentType("application/json;charset=utf-8"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); PrintWriter out = response.getWriter(); Map map = new HashMap<>(); map.put("code", HttpServletResponse.SC_UNAUTHORIZED); map.put("msg", "用户名或者密码错误"); out.write(new ObjectMapper().writeValueAsString(map)); out.flush(); out.close(); } catch (Exception e1) { e1.printStackTrace(); } throw new RuntimeException(e); } } //重写用户名密码受权成功操做----返回token凭证 protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { //从authResult获取认证成功的用户信息 SysUser resultUser = new SysUser(); SysUser authUser = (SysUser) authResult.getPrincipal(); resultUser.setUsername(authUser.getUsername()); resultUser.setId(authUser.getId()); resultUser.setStatus(authUser.getStatus()); resultUser.setRoles((List<SysRole>) authResult.getAuthorities()); String token = JwtUtils.generateTokenExpireInMinutes(resultUser, prop.getPrivateKey(), 3600*24); //将token写入header response.addHeader("Authorization", "Bearer " + token); try { //登陆成功時,返回json格式进行提示 response.setContentType("application/json;charset=utf-8"); response.setStatus(HttpServletResponse.SC_OK); PrintWriter out = response.getWriter(); Map<String, Object> map = new HashMap<String, Object>(); map.put("code", HttpServletResponse.SC_OK); map.put("message", "登录成功!"); out.write(new ObjectMapper().writeValueAsString(map)); out.flush(); out.close(); } catch (Exception e1) { e1.printStackTrace(); } } }
5. 编写检验token过滤器
public class JwtVerifyFilter extends BasicAuthenticationFilter { private RsaKeyProperties prop; public JwtVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) { super(authenticationManager); this.prop = prop; } /** * 过滤请求 */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { try { //请求体的头中是否包含Authorization String header = request.getHeader("Authorization"); //Authorization中是否包含Bearer,不包含直接返回 if (header == null || !header.startsWith("Bearer ")) { chain.doFilter(request, response); responseJson(response); return; } //获取权限失败,会抛出异常 UsernamePasswordAuthenticationToken authentication = getAuthentication(request); //获取后,将Authentication写入SecurityContextHolder中供后序使用 SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(request, response); } catch (Exception e) { responseJson(response); e.printStackTrace(); } } /** * 未登陆提示 * * @param response */ private void responseJson(HttpServletResponse response) { try { //未登陆提示 response.setContentType("application/json;charset=utf-8"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); PrintWriter out = response.getWriter(); Map<String, Object> map = new HashMap<String, Object>(); map.put("code", HttpServletResponse.SC_FORBIDDEN); map.put("message", "请登陆!"); out.write(new ObjectMapper().writeValueAsString(map)); out.flush(); out.close(); } catch (Exception e1) { e1.printStackTrace(); } } /** * 经过token,获取用户信息 * * @param request * @return */ private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { String token = request.getHeader("Authorization"); if (token != null) { //经过token解析出载荷信息 Payload<SysUser> payload = JwtUtils.getInfoFromToken(token.replace("Bearer ", ""), prop.getPublicKey(), SysUser.class); SysUser user = payload.getUserInfo(); //不为null,返回 if (user != null) { return new UsernamePasswordAuthenticationToken(user, null, user.getRoles()); } return null; } return null; } }
6. 编写SpringSecurity配置类
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; @Autowired private RsaKeyProperties prop; @Bean public BCryptPasswordEncoder myPasswordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http //关闭跨站请求防御 .cors() .and() .csrf() .disable() //容许不登录就能够访问的方法,多个用逗号分隔 .authorizeRequests() .antMatchers("/product").hasAnyRole("ROLE_USER") //其余的须要受权后访问 .anyRequest().authenticated() .and() //增长自定义认证过滤器 .addFilter(new JwtLoginFilter(authenticationManager(), prop)) //增长自定义验证认证过滤器 .addFilter(new JwtVerifyFilter(authenticationManager(), prop)) // 先后端分离是无状态的,不用session了,直接禁用。 .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); } @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { //UserDetailsService类 auth.userDetailsService(userService) //加密策略 .passwordEncoder(bCryptPasswordEncoder); } }
7. 启动测试认证服务
认证成功会返回token
4. 资源模块
资源服务能够有不少个,这里只拿产品服务为例,记住,资源服务中只能经过公钥验证认证。不能签发token!
pom.xml
以下
<?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"> <parent> <artifactId>springboot_security_jwt_rsa_parent</artifactId> <groupId>com.example</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>server_resource</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.example</groupId> <artifactId>common</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> <scope>provided</scope> </dependency> </dependencies> </project>
1. 配置application.yml
server: port: 8082 spring: datasource: driver-class-name: com.mysql.jdbc.Driver password: root username: root url: jdbc:mysql:///test mybatis: type-aliases-package: com.example.domain configuration: map-underscore-to-camel-case: true logging: level: com.example: debug rsa: key: publicKeyFile: D:\test\auth\id_rsa_public.pub
2. 编写读取公钥的配置类
@ConfigurationProperties(prefix = "rsa.key") @Data public class RsaKeyProperties { private String publicKeyFile; private PublicKey publicKey; @PostConstruct public void createRsaKey() throws Exception { publicKey = RsaUtils.getPublicKey(publicKeyFile); } }
3. 编写启动类
@SpringBootApplication @MapperScan("com.example.mapper") @EnableConfigurationProperties(RsaKeyProperties.class) public class ProductApplication { public static void main(String[] args) { SpringApplication.run(ProductApplication.class, args); } }
4. 编写检验token过滤器
public class JwtVerifyFilter extends BasicAuthenticationFilter { private RsaKeyProperties prop; public JwtVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) { super(authenticationManager); this.prop = prop; } /** * 过滤请求 */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { try { //请求体的头中是否包含Authorization String header = request.getHeader("Authorization"); //Authorization中是否包含Bearer,不包含直接返回 if (header == null || !header.startsWith("Bearer ")) { chain.doFilter(request, response); responseJson(response); return; } //获取权限失败,会抛出异常 UsernamePasswordAuthenticationToken authentication = getAuthentication(request); //获取后,将Authentication写入SecurityContextHolder中供后序使用 SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(request, response); } catch (Exception e) { responseJson(response); e.printStackTrace(); } } /** * 未登陆提示 * * @param response */ private void responseJson(HttpServletResponse response) { try { //未登陆提示 response.setContentType("application/json;charset=utf-8"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); PrintWriter out = response.getWriter(); Map<String, Object> map = new HashMap<String, Object>(); map.put("code", HttpServletResponse.SC_FORBIDDEN); map.put("message", "请登陆!"); out.write(new ObjectMapper().writeValueAsString(map)); out.flush(); out.close(); } catch (Exception e1) { e1.printStackTrace(); } } /** * 经过token,获取用户信息 * * @param request * @return */ private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { String token = request.getHeader("Authorization"); if (token != null) { //经过token解析出载荷信息 Payload<SysUser> payload = JwtUtils.getInfoFromToken(token.replace("Bearer ", ""), prop.getPublicKey(), SysUser.class); SysUser user = payload.getUserInfo(); //不为null,返回 if (user != null) { return new UsernamePasswordAuthenticationToken(user, null, user.getRoles()); } return null; } return null; } }
5. 编写SpringSecurity配置类
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; @Autowired private RsaKeyProperties prop; @Bean public BCryptPasswordEncoder myPasswordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http //关闭跨站请求防御 .cors() .and() .csrf() .disable() //容许不登录就能够访问的方法,多个用逗号分隔 .authorizeRequests() .antMatchers("/product").hasAnyRole("ROLE_USER") //其余的须要受权后访问 .anyRequest().authenticated() .and() //增长自定义验证认证过滤器 .addFilter(new JwtVerifyFilter(authenticationManager(), prop)) // 先后端分离是无状态的,不用session了,直接禁用。 .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); } @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { //UserDetailsService类 auth.userDetailsService(userService) //加密策略 .passwordEncoder(bCryptPasswordEncoder); } }
6. 编写测试产品控制器
@RestController @RequestMapping("/product") public class ProductController { @Secured("ROLE_PRODUCT") @RequestMapping("/list") public String findAll(){ return "产品测试成功!"; } }
7. 启动产品服务作测试
当不包含ROLE_PRODUCT
权限的用户访问时会被阻止
当包含ROLE_PRODUCT
权限的用户认证后会被容许访问
4. 实例代码
5. 参考资源
企业开发首选的安全框架Spring Security深刻浅出
感谢黑马程序员的课程分享