创立我的项目
生成我的项目模板
为不便咱们初始化我的项目,Spring Boot 给咱们提供一个我的项目模板生成网站。
1. 关上浏览器,拜访:https://start.spring.io/
2. 依据页面提醒,抉择构建工具,开发语言,我的项目信息等。
3. 点击 Generate,生成我的项目模板,生成之后会将压缩包下载到本地。
4. 解压 demo.zip 文件,而后 idea 导入 pom.xml 文件,我的项目构造如下:
增加相干依赖
增加 Maven 相干依赖,这里须要增加上 web、swagger、spring security、jwt 和 fastjson 的依赖,Swagge 和 fastjson 的增加是为了不便接口测试。
<?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>
<parent> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.spring.security</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties> <java.version>1.8</java.version>
</properties>
<dependencies> <!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency> <dependency> <groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency> <!-- spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency> <!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency> <!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency> <!-- 集成 lombok 框架 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin> </plugins> <!-- 打包时拷贝 MyBatis 的映射文件 -->
<resources>
<resource> <directory>src/main/java</directory>
<includes> <include>**/sqlmap/*.xml</include>
</includes> <filtering>false</filtering>
</resource> <resource> <directory>src/main/resources</directory>
<includes> <include>**/*.*</include>
</includes> <filtering>true</filtering>
</resource> </resources> </build>
</project>
增加相干配置
- 增加 swagger 配置
增加一个 swagger 配置类,在工程下新建 config 包并增加一个 SwaggerConfig
配置类,除了惯例配置外,加了一个令牌属性,能够在接口调用的时候传递令牌。SwaggerConfig.java
package com.spring.security.demo.config;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* @Author wulongbo
* @Date 2020/11/21 11:50
* @Version 1.0
*/@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi(){
// 增加申请参数,咱们这里把 token 作为申请头部参数传入后端
ParameterBuilder parameterBuilder = new ParameterBuilder();
List<Parameter> parameters = new ArrayList<Parameter>();
parameterBuilder.name("Authorization").description("令牌").modelRef(new ModelRef("string")).parameterType("header")
.required(false).build();
parameters.add(parameterBuilder.build());
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any()).build().globalOperationParameters(parameters);
}
private ApiInfo apiInfo(){return new ApiInfoBuilder()
.title("SpringBoot API Doc")
.description("This is a restful api document of Spring Boot.")
.version("1.0")
.build();}
}
- 增加跨域 配置
增加一个 CORS 跨域配置类,在工程下新建 config 包并增加一个 CorsConfig
配置类。CorsConfig.java
package com.spring.security.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @Author wulongbo
* @Date 2020/11/21 11:52
* @Version 1.0
*/@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**") // 容许跨域拜访的门路
.allowedOrigins("*") // 容许跨域拜访的源
.allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE") // 容许申请办法
.maxAge(168000) // 预检间隔时间
.allowedHeaders("*") // 容许头部设置
.allowCredentials(true); // 是否发送 cookie
}
}
-
平安配置类
上面这个配置类是 Spring Security 的要害配置。
在这个配置类中,咱们次要做了以下几个配置:
- 拜访门路 URL 的受权策略,如登录、Swagger 拜访免登录认证等
- 指定了登录认证流程过滤器 JwtLoginFilter,由它来触发登录认证
- 指定了自定义身份认证组件 JwtAuthenticationProvider,并注入 UserDetailsService
- 指定了访问控制过滤器 JwtAuthenticationFilter,在受权时解析令牌和设置登录状态
- 指定了退出登录处理器,因为是前后端拆散,避免内置的登录处理器在后盾进行跳转
WebSecurityConfig.java
package com.spring.security.demo.config;
import com.spring.security.demo.security.JwtAuthenticationFilter;
import com.spring.security.demo.security.JwtAuthenticationProvider;
import com.spring.security.demo.security.JwtLoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {// 应用自定义登录身份认证组件 ----> 指定了自定义身份认证组件 JwtAuthenticationProvider,并注入 UserDetailsService auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService));
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 禁用 csrf, 因为应用的是 JWT,咱们这里不须要 csrf
http.cors().and().csrf().disable()
.authorizeRequests()
// 跨域预检申请
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 登录 URL
.antMatchers("/login").permitAll()
// swagger
.antMatchers("/swagger**/**").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/v2/**").permitAll()
// 其余所有申请须要身份认证
.anyRequest().authenticated();
// 退出登录处理器
http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
// 开启登录认证流程过滤器 ----> 指定了登录认证流程过滤器 JwtLoginFilter,由它来触发登录认证
http.addFilterBefore(new JwtLoginFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
// 访问控制时登录状态查看过滤器 ----> 指定了访问控制过滤器 JwtAuthenticationFilter,在受权时解析令牌和设置登录状态
http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override public AuthenticationManager authenticationManager() throws Exception {return super.authenticationManager();
}
}
登录认证触发过滤器
JwtLoginFilter 是在通过拜访 /login 的 POST 申请是被首先被触发的过滤器,默认实现是 UsernamePasswordAuthenticationFilter,它继承了 AbstractAuthenticationProcessingFilter,形象父类的 doFilter 定义了登录认证的大抵操作流程,这里咱们的 JwtLoginFilter 继承了 UsernamePasswordAuthenticationFilter,并进行了两个次要内容的定制。
- 覆写认证办法,批改用户名、明码的获取形式,具体起因看代码正文
- 覆写认证胜利后的操作,移除后盾跳转,增加生成令牌并返回给客户端
JwtLoginFilter.java
package com.spring.security.demo.security;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.spring.security.demo.utils.HttpUtils;
import com.spring.security.demo.utils.JwtTokenUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
/**
* 启动登录认证流程过滤器
* @author Wulongbo
* @date Nov 28, 2020
*/public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {public JwtLoginFilter(AuthenticationManager authManager) {setAuthenticationManager(authManager);
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
// POST 申请 /login 登录时拦挡,由此办法触发执行登录认证流程,能够在此覆写整个登录认证逻辑
super.doFilter(req, res, chain);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 能够在此覆写尝试进行登录认证的逻辑,登录胜利之后等操作不再此办法内
// 如果应用此过滤器来触发登录认证流程,留神登录申请数据格式的问题
// 此过滤器的用户名明码默认从 request.getParameter() 获取,然而这种
// 读取形式不能读取到如 application/json 等 post 申请数据,须要把
// 用户名明码的读取逻辑批改为到流中读取 request.getInputStream()
String body = getBody(request);
JSONObject jsonObject = JSON.parseObject(body);
String username = jsonObject.getString("username");
String password = jsonObject.getString("password");
if (username == null) {username = "";}
if (password == null) {password = "";}
username = username.trim();
JwtAuthenticatioToken authRequest = new JwtAuthenticatioToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 存储登录认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authResult);
// 记住我服务
getRememberMeServices().loginSuccess(request, response, authResult);
// 触发事件监听器
if (this.eventPublisher != null) {eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
// 生成并返回 token 给客户端,后续拜访携带此 token
JwtAuthenticatioToken token = new JwtAuthenticatioToken(null, null, JwtTokenUtils.generateToken(authResult));
HttpUtils.write(response, token);
}
/**
* 获取申请 Body
* @param request
* @return
*/
public String getBody(HttpServletRequest request) {StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {inputStream = request.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = "";
while ((line = reader.readLine()) != null) {sb.append(line);
}
} catch (IOException e) {e.printStackTrace();
} finally {if (inputStream != null) {
try {inputStream.close();
} catch (IOException e) {e.printStackTrace();
}
}
if (reader != null) {
try {reader.close();
} catch (IOException e) {e.printStackTrace();
}
}
}
return sb.toString();}
}
登录控制器
除了应用下面的登录认证过滤器拦挡 /login Post 申请之外,咱们也能够不应用下面的过滤器,通过自定义登录接口实现,只有在登录接口手动触发登录流程并生产令牌即可。
其实 Spring Security 的登录认证过程只需调用 AuthenticationManager 的 authenticate(Authentication authentication) 办法,最终返回认证胜利的 Authentication 实现类并存储到 SpringContexHolder 上下文即可,这样前面受权的时候就能够从 SpringContexHolder 中获取登录认证信息,并依据其中的用户信息和权限信息决定是否进行受权。LoginController.java
package com.spring.security.demo.controller;
import com.spring.security.demo.security.JwtAuthenticatioToken;
import com.spring.security.demo.utils.SecurityUtils;
import com.spring.security.demo.vo.HttpResult;
import com.spring.security.demo.vo.LoginBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* @Author wulongbo
* @Date 2020/11/24 10:05
* @Version 1.0
*/
@RestController
public class LoginController {
@Autowired
private AuthenticationManager authenticationManager;
/**
* 登录接口
*/
@PostMapping(value = "/login")
public HttpResult login(@RequestBody LoginBean loginBean, HttpServletRequest request) throws IOException {String username = loginBean.getUsername();
String password = loginBean.getPassword();
// 零碎登录认证
JwtAuthenticatioToken token = SecurityUtils.login(request, username, password, authenticationManager);
return HttpResult.ok(token);
}
}
如下是登录认证的逻辑,能够看到局部逻辑跟下面的登录认证过滤器差不多。
- 执行登录认证过程,通过调用 AuthenticationManager 的 authenticate(token) 办法实现
- 将认证胜利的认证信息存储到上下文,供后续拜访受权的时候获取应用
- 通过 JWT 生成令牌并返回给客户端,后续拜访和操作都须要携带此令牌
无关登录过程的逻辑,参见 SecurityUtils 的 login 办法。
SecurityUtils.java
package com.spring.security.demo.utils;
import javax.servlet.http.HttpServletRequest;
import com.spring.security.demo.security.JwtAuthenticatioToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
/**
* Security 相干操作
* @author Wulongbo
* @date Jun 29, 2020
*/public class SecurityUtils {
/**
* 零碎登录认证
* @param request
* @param username
* @param password
* @param authenticationManager
* @return
*/
public static JwtAuthenticatioToken login(HttpServletRequest request, String username, String password, AuthenticationManager authenticationManager) {JwtAuthenticatioToken token = new JwtAuthenticatioToken(username, password);
token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 执行登录认证过程
Authentication authentication = authenticationManager.authenticate(token);
// 认证胜利存储认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成令牌并返回给客户端
token.setToken(JwtTokenUtils.generateToken(authentication));
return token;
}
/**
* 获取令牌进行认证
* @param request
*/
public static void checkAuthentication(HttpServletRequest request) {
// 获取令牌并依据令牌获取登录认证信息
Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request);
// 设置登录认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
}
/**
* 获取以后用户名
* @return
*/
public static String getUsername() {
String username = null;
Authentication authentication = getAuthentication();
if(authentication != null) {Object principal = authentication.getPrincipal();
if(principal != null && principal instanceof UserDetails) {username = ((UserDetails) principal).getUsername();}
}
return username;
}
/**
* 获取用户名
* @return
*/
public static String getUsername(Authentication authentication) {
String username = null;
if(authentication != null) {Object principal = authentication.getPrincipal();
if(principal != null && principal instanceof UserDetails) {username = ((UserDetails) principal).getUsername();}
}
return username;
}
/**
* 获取以后登录信息
* @return
*/
public static Authentication getAuthentication() {if(SecurityContextHolder.getContext() == null) {return null;}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication;
}
}
令牌生成器
咱们令牌是应用 JWT 生成的,令牌生成的逻辑,参见源码 JwtTokenUtils 的 generateToken 相干办法。
JwtTokenUtils.java
package com.spring.security.demo.utils;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import com.spring.security.demo.security.GrantedAuthorityImpl;
import com.spring.security.demo.security.JwtAuthenticatioToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
/**
* JWT 工具类
* @author Wulongbo
* @date Jun 29, 2020
*/public class JwtTokenUtils implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户名称
*/
private static final String USERNAME = Claims.SUBJECT;
/**
* 创立工夫
*/
private static final String CREATED = "created";
/**
* 权限列表
*/
private static final String AUTHORITIES = "authorities";
/**
* 密钥
*/
private static final String SECRET = "abcdefgh";
/**
* 有效期 12 小时
*/
private static final long EXPIRE_TIME = 12 * 60 * 60 * 1000;
/**
* 生成令牌
*
* @param authentication 用户
* @return 令牌
*/
public static String generateToken(Authentication authentication) {Map<String, Object> claims = new HashMap<>(3);
claims.put(USERNAME, SecurityUtils.getUsername(authentication));
claims.put(CREATED, new Date());
claims.put(AUTHORITIES, authentication.getAuthorities());
return generateToken(claims);
}
/**
* 从数据申明生成令牌
*
* @param claims 数据申明
* @return 令牌
*/
private static String generateToken(Map<String, Object> claims) {Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact();}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
public static String getUsernameFromToken(String token) {
String username;
try {Claims claims = getClaimsFromToken(token);
username = claims.getSubject();} catch (Exception e) {username = null;}
return username;
}
/**
* 依据申请令牌获取登录认证信息
* @param token 令牌
* @return 用户名
*/
public static Authentication getAuthenticationeFromToken(HttpServletRequest request) {
Authentication authentication = null;
// 获取申请携带的令牌
String token = JwtTokenUtils.getToken(request);
if(token != null) {
// 申请令牌不能为空
if(SecurityUtils.getAuthentication() == null) {
// 上下文中 Authentication 为空
Claims claims = getClaimsFromToken(token);
if(claims == null) {return null;}
String username = claims.getSubject();
if(username == null) {return null;}
if(isTokenExpired(token)) {return null;}
Object authors = claims.get(AUTHORITIES);
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
if (authors != null && authors instanceof List) {for (Object object : (List) authors) {authorities.add(new GrantedAuthorityImpl((String) ((Map) object).get("authority")));
}
}
authentication = new JwtAuthenticatioToken(username, null, authorities, token);
} else {if(validateToken(token, SecurityUtils.getUsername())) {
// 如果上下文中 Authentication 非空,且申请令牌非法,间接返回以后登录认证信息
authentication = SecurityUtils.getAuthentication();}
}
}
return authentication;
}
/**
* 从令牌中获取数据申明
*
* @param token 令牌
* @return 数据申明
*/
private static Claims getClaimsFromToken(String token) {
Claims claims;
try {claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();} catch (Exception e) {claims = null;}
return claims;
}
/**
* 验证令牌
* @param token
* @param username
* @return
*/
public static Boolean validateToken(String token, String username) {String userName = getUsernameFromToken(token);
return (userName.equals(username) && !isTokenExpired(token));
}
/**
* 刷新令牌
* @param token
* @return
*/
public static String refreshToken(String token) {
String refreshedToken;
try {Claims claims = getClaimsFromToken(token);
claims.put(CREATED, new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {refreshedToken = null;}
return refreshedToken;
}
/**
* 判断令牌是否过期
*
* @param token 令牌
* @return 是否过期
*/
public static Boolean isTokenExpired(String token) {
try {Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {return false;}
}
/**
* 获取申请 token
* @param request
* @return
*/
public static String getToken(HttpServletRequest request) {String token = request.getHeader("Authorization");
String tokenHead = "Bearer";
if(token == null) {token = request.getHeader("token");
} else if(token.contains(tokenHead)){token = token.substring(tokenHead.length());
}
if("".equals(token)) {token = null;}
return token;
}
}
登录身份认证组件
下面说到登录认证是通过调用 AuthenticationManager 的 authenticate(token) 办法实现的,而 AuthenticationManager 又是通过调用 AuthenticationProvider 的 authenticate(Authentication authentication) 来实现认证的,所以通过定制 AuthenticationProvider 也能够实现各种自定义的需要,咱们这里只是简略的继承 DaoAuthenticationProvider 展现如何自定义,具体的大家能够依据各自的需要按需定制。
JwtAuthenticationProvider.java
package com.spring.security.demo.security;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* 身份验证提供者
* @author Wulongbo
* @date Nov 20, 2020
*/public class JwtAuthenticationProvider extends DaoAuthenticationProvider {public JwtAuthenticationProvider(UserDetailsService userDetailsService) {setUserDetailsService(userDetailsService);
setPasswordEncoder(new BCryptPasswordEncoder());
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 能够在此处覆写整个登录认证逻辑
return super.authenticate(authentication);
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
// 能够在此处覆写明码验证逻辑
super.additionalAuthenticationChecks(userDetails, authentication);
}
}
咱们自定义的 UserDetailsService,从咱们的用户服务 UserService 中获取用户和权限信息。
UserDetailsServiceImpl.java
package com.spring.security.demo.security;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import com.spring.security.demo.model.User;
import com.spring.security.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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;
/**
* 用户登录认证信息查问
* @author Wulongbo
* @date Jun 29, 2020
*/@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userService.findByUsername(username);
if (user == null) {throw new UsernameNotFoundException("该用户不存在");
}
// 用户权限列表,依据用户领有的权限标识与如 @PreAuthorize("hasAuthority('sys:menu:view')") 标注的接口比照,决定是否能够调用接口
Set<String> permissions = userService.findPermissions(username);
List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList());
return new JwtUserDetails(username, user.getPassword(), grantedAuthorities);
}
}
用户认证信息
下面 UserDetailsService 加载好用户认证信息后会封装认证信息到一个 UserDetails 的实现类。
默认实现是 User 类,咱们这里没有非凡须要,简略继承即可,简单需要能够在此基础上进行拓展。
JwtUserDetails.java
package com.spring.security.demo.security;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
/**
* 平安用户模型
* @author Wulongbo
* @date Jun 29, 2020
*/public class JwtUserDetails extends User {
private static final long serialVersionUID = 1L;
public JwtUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {this(username, password, true, true, true, true, authorities);
}
public JwtUserDetails(String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
}
用户操作代码
简略的用户模型,蕴含用户名明码。
User.java
package com.spring.security.demo.model;
import lombok.Data;
/**
* 用户模型
* @author Wulongbo
* @date Jun 29, 2020
*/@Data
public class User {
private Long id;
private String username;
private String password;
}
用户服务接口,只提供简略的用户查问和权限查问接口用于模仿。
UserService.java
package com.spring.security.demo.service;
import com.spring.security.demo.model.User;
import java.util.Set;
/**
* 用户治理
* @author Wulongbo
* @date Jun 29, 2020
*/public interface UserService {
/**
* 依据用户名查找用户
* @param username
* @return
*/
User findByUsername(String username);
/**
* 查找用户的菜单权限标识汇合
* @param username
* @return
*/
Set<String> findPermissions(String username);
}
用户服务实现,只简略获取返回模仿数据,理论场景依据状况从 DAO 获取即可。
SysUserServiceImpl.java
package com.spring.security.demo.service.impl;
import java.util.HashSet;
import java.util.Set;
import com.spring.security.demo.model.User;
import com.spring.security.demo.service.UserService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class SysUserServiceImpl implements UserService {
@Override
public User findByUsername(String username) {User user = new User();
user.setId(1L);
user.setUsername(username);
String password = new BCryptPasswordEncoder().encode("123");
user.setPassword(password);
return user;
}
@Override
public Set<String> findPermissions(String username) {Set<String> permissions = new HashSet<>();
permissions.add("sys:user:view");
permissions.add("sys:user:add");
permissions.add("sys:user:edit");
permissions.add("sys:user:delete");
return permissions;
}
}
用户控制器,提供三个测试接口,其中权限列表中未蕴含删除接口定义的权限(’sys:user:delete’),登录之后也将无权限调用。
UserController.java
package com.spring.security.demo.controller;
import com.spring.security.demo.vo.HttpResult;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 用户控制器
* @author Wulongbo
* @date Jun 29, 2020
*/
@RestController
@RequestMapping("user")
public class UserController {@PreAuthorize("hasAuthority('sys:user:view')")
@GetMapping(value="/findAll")
public HttpResult findAll() {return HttpResult.ok("the findAll service is called success.");
}
@PreAuthorize("hasAuthority('sys:user:edit')")
@GetMapping(value="/edit")
public HttpResult edit() {return HttpResult.ok("the edit service is called success.");
}
@PreAuthorize("hasAuthority('sys:user:delete')")
@GetMapping(value="/delete")
public HttpResult delete() {return HttpResult.ok("the delete service is called success.");
}
}
登录认证查看过滤器
拜访接口的时候,登录认证查看过滤器 JwtAuthenticationFilter 会拦挡申请校验令牌和登录状态,并依据状况设置登录状态。
JwtAuthenticationFilter.java
package com.spring.security.demo.security;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.spring.security.demo.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
/**
* 登录认证查看过滤器
* @author Wulongbo
* @date Jun 29, 2020
*/public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 获取 token, 并查看登录状态
SecurityUtils.checkAuthentication(request);
chain.doFilter(request, response);
}
}
具体具体获取 token 和查看登录状态代码请查看 SecurityUtils 的 checkAuthentication 办法。
编译测试运行
- 我的项目目录构造如下,Maven install,开始执行 Maven 构建, 如果呈现如下信息,就阐明我的项目编译打包胜利了。
- 关上文件文件 DemoApplication.java -> Run DemoApplication,开始启动利用,当呈现如下信息的时候,就阐明利用启动胜利了,默认启动端口是 8080
- 关上浏览器,拜访:http://localhost:8080/swagger-ui.html,进入 swagger 接口文档界面。