Spring Security是一个基于Spring框架的权限管理框架,用于帮助应用程序实现身份验证和授权功能。它可以为Web应用程序、REST API和方法级安全性提供支持,并支持各种认证方式。
Spring Security最初是Acegi Security的前身,但由于其配置繁琐而受到批评。随着Spring Boot的出现,Spring Security的易用性得到了极大的提高,成为了Spring Boot和Spring Cloud项目中常用的安全框架。
Spring Security的基本功能包括认证和授权。认证方面,它支持多种常见的认证方式,例如基于表单的认证、HTTP基本认证、OpenID Connect、OAuth2等。授权方面,它提供了基于URL的请求授权、支持方法访问授权以及对象访问授权等能力,可用于限制用户对应用程序中资源的访问。除此之外,Spring Security还提供了一些其他的安全特性,例如CSRF防护、会话管理等,以帮助应用程序保护安全性和保密性。
Spring Security是一个强大的安全性框架,它被广泛用于基于Java的Web应用程序中。它基于Servlet过滤器实现了一套标准化的认证和授权机制,通过一系列Filter来处理Web请求,以确保只有经过身份验证的用户可以访问系统中的受保护资源。
在Spring Security中,Filter链是一个重要的概念。它由多个Filter组成,每个Filter都负责执行不同的任务,例如身份验证、授权、防止CSRF攻击等。这些Filter按照特定的顺序组成了一个链条,每当一个请求到达应用程序时,请求将被传递给Filter链,直到找到合适的Filter进行处理或者抛出异常。
在Filter链中,认证和授权通常是最核心的部分。Spring Security提供了各种方式来进行身份验证和授权,例如表单登录、基本认证、OAuth2等。在处理过程中,如果出现任何异常,如认证失败或权限不足,Spring Security将会抛出异常并将其传递给异常处理器进行处理。异常处理器通常会捕获异常、记录日志并向用户显示错误消息,以便及时解决问题。
总之,Filter链是Spring Security中非常重要的一环,它能够为我们的Web应用程序提供强大的安全性保障。通过组织不同的Filter,Spring Security可以提供多种不同的身份验证和授权机制,使我们能够轻松地保护应用程序中的敏感资源。
除了上述提到的Spring Security常用组件外,还有以下一些组件:
AccessDecisionManager:用于根据用户和资源的相关信息判断是否允许用户访问资源。
AuthenticationEntryPoint:如果一个未认证的用户试图访问需要认证的资源,会被重定向到该接口实现的方法处理。
AuthenticationProvider:用于对用户进行认证并生成认证对象 Authentication。
FilterSecurityInterceptor:在请求到达后台之前进行拦截和处理,包含很多安全检查点。
RememberMeAuthenticationProvider:为支持“记住我”功能提供的认证处理器,用于生成认证对象 Authentication。
SessionRegistry:用于跟踪已经登录的用户,通常在实现“单点登录”时使用。
这些组件可以通过配置文件中的bean来进行自定义,并且可以根据具体情况进行组合搭配,以实现更加灵活、高效的安全管理方案。
引入 Spring Security 依赖
<!--引入 Spring Security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
引入依赖后,不做任何配置,Spring Security 会自动生效,请求将跳转登录页面
默认用户名、密码和权限可在 application.yaml 中配置
@Configuration
@EnableWebSecurity
// 开启注解设置权限
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 配置密码加密器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 配置认证管理器
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password(passwordEncoder().encode("123")).roles("admin")
.and()
.withUser("user")
.password(passwordEncoder().encode("456")).roles("user");
}
// 配置安全策略
@Override
protected void configure(HttpSecurity http) throws Exception {
// 设置路径及要求的权限,支持 ant 风格路径写法
http.authorizeRequests()
// 设置 OPTIONS 尝试请求直接通过
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers("/api/demo/user").hasAnyRole("user", "admin")
// 注意使用 hasAnyAuthority 角色需要以 ROLE_ 开头
.antMatchers("/api/demo/admin").hasAnyAuthority("ROLE_admin")
.antMatchers("/api/demo/hello").permitAll()
.and()
// 开启表单登录
.formLogin().permitAll()
.and()
// 开启注销
.logout().permitAll();
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭 csrf 防御
http.csrf().disable();
// 关闭会话管理
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// ...
}
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 判断是否为 JSON 格式请求
if(request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
// ...
} else {
return super.attemptAuthentication(request, response);
}
}
}
@Autowired
private CustomUserDetailsService customUserDetailsService
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder());
}
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 根据 username 查询用户
User user = userMapper.getUserByUsername(s);
if (user == null) {
// ...
}
// 查询角色或权限
List<SimpleGrantedAuthority> authorities = userMapper.listRolesByUsername(s)
.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// 构造 UserDetails 实例并返回
}
}
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 根据 username 查询用户
User user = userMapper.getUserByUsername(s);
if (user == null) {
// ...
}
// 查询角色或权限
List<SimpleGrantedAuthority> authorities = userMapper.listRolesByUsername(s)
.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// 构造 UserDetails 实例并返回
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().permitAll()
.loginProcessingUrl("/login")
.successHandler(customLoginSuccessHandler)
}
CustomLoginSuccessHandler,以 JSON 形式返回前端,携带生成的 Token
@Component
@RequiredArgsConstructor
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {
private final JwtUtil jwtUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
// 构造一个统一返回格式对象
Map<String, Object> res = new HashMap<>();
res.put("code", 200);
res.put("message": "认证成功");
res.put("path": "login");
Object principal = authentication.getPrincipal();
if (principal instanceof User) {
// 根据用户信息,使用 JWT 工具类构建 Token
// ...
// 存到返回内容中
res.put("data", "xxxxxx")
}
// 以 JSON 格式写入 response
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.print(JsonUtil.Obj2Str(res));
writer.flush();
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().permitAll()
.loginProcessingUrl("/login")
.failureHandler(customLoginFailureHandler)
}
@Component
public class CustomLoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) {
// 封装的统一返回格式对象
Res<Object> res = Res.of(ResCode.TOKEN_CREATE_FAIL).path("/login");
// 根据异常设置失败信息
if (exception instanceof LockedException) {
res.errorMsg("账户被锁定");
} else if (exception instanceof CredentialsExpiredException) {
res.errorMsg("密码过期");
} else if (exception instanceof AccountExpiredException) {
res.errorMsg("账户过期");
} else if (exception instanceof DisabledException) {
res.errorMsg("账户被禁用");
} else if (exception instanceof BadCredentialsException) {
res.errorMsg("用户名或者密码输入错误");
}
// 封装的 JSON 格式写入 response 工具方法
WebUtil.writeJsonToResponse(response, JsonUtil.objToStr(res));
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(customAuthenticationEntryPoint)
}
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// 构造未登录的返回内容
Res<Object> res = Res.of(ResCode.TOKEN_NOT_EXIST)
.path(request.getRequestURI());
WebUtil.writeJsonToResponse(response, JsonUtil.objToStr(res));
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.accessDeniedHandler(customAccessDeniedHandler);
}
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 构造权限不足的返回内容
Res<Object> res = Res.of(ResCode.TOKEN_NO_AUTHORITY)
.path(request.getRequestURI());
WebUtil.writeJsonToResponse(response, JsonUtil.objToStr(res));
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.logout().permitAll()
.logoutUrl("/logout")
.logoutSuccessHandler(logoutSuccessHandler);
}
@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
// 构造注销成功的返回内容
Res<String> res = Res.ok("注销成功").path("/logout");
WebUtil.writeJsonToResponse(response, JsonUtil.objToStr(res));
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(jwtAuthenticationTokenFilter,
UsernamePasswordAuthenticationFilter.class);
}
@Component
@RequiredArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService;
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
// 取出 header 中的 token 进行校验
String authHeader = httpServletRequest.getHeader(jwtUtil.getHeader());
if (authHeader != null && !StringUtil.isEmpty(authHeader)) {
String username = jwtUtil.getUsernameFromToken(authHeader);
if (username != null
&& SecurityContextHolder.getContext().getAuthentication() == null) {
// 根据 username 查询用户,可以从缓存、数据库中获取
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 校验
if (jwtUtil.validateToken(authHeader, userDetails)) {
// 构建 authentication
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails,
null,
userDetails.getAuthorities());
// 设置 details,其中包含地址、session 等
authentication.setDetails(new
WebAuthenticationDetails(httpServletRequest));
// 设置 authentication 到上下文对象中
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
private final FilterInvocationSecurityMetadataSource superMetadataSource;
private final Map<String, String[]> urlRoleMap = new HashMap<>();
public MySecurityMetadataSource(
FilterInvocationSecurityMetadataSource metadataSource) {
this.superMetadataSource = metadataSource;
// 此处可以从数据库加载权限配置
urlRoleMap.put("/api/demo/admin", new String[]{"ROLE_admin"});
urlRoleMap.put("/api/demo/user", new String[]{"ROLE_user", "ROLE_admin"});
}
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
FilterInvocation fi = (FilterInvocation) object;
String url = fi.getRequestUrl();
for (Map.Entry<String, String[]> entry : urlRoleMap.entrySet()) {
if (antPathMatcher.match(entry.getKey(), url)) {
// 生成 ConfigAttribute
return SecurityConfig.createList(entry.getValue());
}
}
// 返回配置类定义的默认权限配置
return superMetadataSource.getAttributes(object);
}
}
http.authorizeRequests()
.anyRequest().authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
// 设置为自定义的 SecurityMetadataSource
object.setSecurityMetadataSource(mySecurityMetadataSource);
// AffirmativeBased 是 AccessDecisionManager 的一种
// AffirmativeBased,有一个投票器通过就通过
// UnanimousBased,有一个投票器不通过就不通过,全部弃权也不通过
object.setAccessDecisionManager(new AffirmativeBased(
Arrays.asList(
new WebExpressionVoter(),
new RoleVoter()
)));
return object;
}
})
/**
* 如果使用 UnanimousBased
* 到达 RoleVoter 的 ConfigAttribute 是从数据库动态获取的,可能有多个
* UnanimousBased 对每个 ConfigAttribute 进行投票,即所有权限都有才算通过
*/
po, dto,vo
post body请求参数,命名规范 XxRequest
展示层对象命名,XxVo
数据传输对象命名,XxDto
es实体名命名 XxIndexDO
db实体命名 跟表名相同
mongo实体命名 XxDoc
db组合关联实体命名 Xx
service接口命名 XxService
service实现命名 XxServiceImpl
manager,service引入多个manager进行负责的组合业务处理 XxManager
dao层命名 XxMapper
封装持久组合服务 XxRepository
apitest
bean
dto
CoolBoyDto
CoolGirlDto
po
CoolBoy
CoolGril
vo
CoolBoyVo
GrilTypeEnums
cache
converter
BoyGirlConverter
model
repository
request
service
![22a417e9ccda4352b4b6ea5c6c33230d.png](https://i-blog.csdnimg.cn/blog_migrate/378d239a3d13461d58e14747fb878b0e.png)
logback.xml
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
server {
listen 80;
server_name xx.com;
charset utf-8;
location / {
alias xxn-front/dist/;
try_files $uri $uri/ /index.html;
index index.html index.htm;
}
location /api {
proxy_pass http://localhost:xxx/api;
proxy_set_header x-forwarded-for $remote_addr;
}
}
![07bdfbc137e4e958f5391580319fa68f.png](https://i-blog.csdnimg.cn/blog_migrate/8d841e2f18e0dc837702a533b4f37284.png)
![2a7db92a6128691f18fe885554b9a0ab.png](https://i-blog.csdnimg.cn/blog_migrate/c5d2fe2e47783c62182add544b052ddf.png)
![700a5a5614b26c1ba0a2a8f95e68d12d.png](https://i-blog.csdnimg.cn/blog_migrate/adc3642ed791e4b1f1c5fac97b8c5767.png)
##qq登陆相关##
qq.app.id=xxx
qq.app.key=xxx
qq.url.authorization=https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=%s&redirect_uri=%s&state=%s
qq.url.access.token=https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s
qq.url.openid=https://graph.qq.com/oauth2.0/me?access_token=%S
qq.url.user.info=https://graph.qq.com/user/get_user_info?access_token=%s&oauth_consumer_key=%s&openid=%s
qq.url.redirect=http://easypan.wuhancoder.com/qqlogincalback
邮箱配置
1、邮箱配置
#发送邮件的邮箱,建议就试用qq邮箱
spring.mail.username=test@qq.com
#发送邮箱的密码
spring.mail.password=123
qq登录:
设置->账户->POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务
![d5359ec266ed7efd291df32ebccd2deb.png](https://i-blog.csdnimg.cn/blog_migrate/949faa0e0a0b295144a9c160f1304914.png)
微信登录QQ邮箱:
个人头像->设置->第三方服务
安装Redis
下载Redis
下载地址:https://wwur.lanzout.com/iD8Ow0ti96dg 密码:4y2e
安装
解压直接双击安装,无需修改配置,一路下一步即可
![a74ae953de31f126ec929bfb79fcbac7.png](https://i-blog.csdnimg.cn/blog_migrate/68e28c1e178db0a11dc8ec841ec0154b.png)
安装ffmpeg
下载
下载地址:https://wwur.lanzout.com/iORvc0tia6uj 密码:9n15
查看版本
然后开启doc执行ffmpeg -verison
搜索gitlab镜像
由于Mac M1芯片区别去Intel,所以在找镜像的时候需要勾选ARM 64,然后一般推荐的镜像就是gitlab-ce。
![8b3dabcecf3ca27204c2a7eafb9d28cb.png](https://i-blog.csdnimg.cn/blog_migrate/0d1bce4dc26fbf6660524ecde2099359.png)
![7e305478366016ab7bc67ea0886adbad.png](https://i-blog.csdnimg.cn/blog_migrate/4d72a2615164470ced3b138c6298b6f7.png)
docker run \
-itd \
--detach \
--restart always \
--name gitlab-ce \
--privileged \
--memory 4096M \
--publish 9922:22 \
--publish 9980:80 \
--volume 在本地创建一个文件夹保存映射的文件/etc:/etc/gitlab:z \
--volume 在本地创建一个文件夹保存映射的文件/log:/var/log/gitlab:z \
--volume 在本地创建一个文件夹保存映射的文件/opt:/var/opt/gitlab:z \
yrzr/gitlab-ce-arm64v8:latest
// 进入容器
docker exec -it gitlab-ce /bin/bash
// 修改gitlab.rb 如图1
vi /etc/gitlab/gitlab.rb
// 在最下面加入以下代码
// gitlab地址,端口默认为80端口
external_url 'http://192.168.124.194'
// ssh主机ip
gitlab_rails['gitlab_ssh_host'] = '192.168.124.194'
// ssh连接端口
gitlab_rails['gitlab_shell_ssh_port'] = 9922
// 修改http和ssh配置,如图2
vi /opt/gitlab/embedded/service/gitlab-rails/config/gitlab.yml
注意此处的host为线上服务器IP,或者改为域名,如果没有则不需要修改
// 修改成功后重启
gitlab-ctl restart
// 退出容器
exit
// 进入容器
docker exec -it gitlab /bin/bask
// 进入控制台
gitlab-rails console -e production
// 查询id为1的账号,1默认是超级管理员
User.where(id:1).first
// 修改密码 密码如果只有数字无法保存
user.password='abc123456'
// 保存修改 如果返回true则表示保存成功
user.save!
// 退出容器
exit
portainer是一款Docker可视化工具,可以方便我们查看和管理Container和Image
打开终端输入命令敲回车
docker run -d -v "/var/run/docker.sock:/var/run/docker.sock" -p 9000:9000 portainer/portainer
安装完成之后运行
docker run -d -p 9000:9000 --restart=always -v /var/run/docker.sock:/var/run/docker.sock --name portainer docker.io/portainer/portainer
浏览器打开 localhost:9000
Lombok(不建议)
@Getter/@Setter
@ToString
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
@Data
@Value
@Builder
@Slf4j
缺点依赖jdk,版本,插件
![cbba7df93077fbbdb574ba65a8e33436.png](https://i-blog.csdnimg.cn/blog_migrate/e8e254cdf7d338896586f38fa1ba0dc3.png)
![99c838783b2abdb9da33a87f25426695.png](https://i-blog.csdnimg.cn/blog_migrate/ced30932367cef3f15ff2cbfa9511d22.png)
<!--jsonwebtoken 生成token的库 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
TokenService
createToken
parseToken
![c5bf1ccd8fc4bfc9361bc2164cc9711f.png](https://i-blog.csdnimg.cn/blog_migrate/c586b650173d39c9c4b8fa0ba3bf4551.png)
Claims claims = Jwts.parser() .setSigningKey("my-123") .parseClaimsJws(token) .getBody();
接口
@RequestMapping(value = '/login')
public Object vLogin(@RequestParam(value = "username") String username, @RequestParam(value = "password") String password) {
Map<String, Object> map = new HashMap<>();
// if (TextUtils.Isempty(username) || TextUtils.Isempty(password)) {
// else
User getUser = userService.validLogin(username, password);
// 如果用户
// getUser != null
if(getUser!=null){
String token=CreateJwt.getoken(getUser);
map.put("user",getUser);
map.put("token",token);
map.put("msg", "登录成功");
}
刷新
@RequestMapping("/tokensign")
public Object tokenSign(@RequestParam(value = "token")String token){
Map<String,Object>map=new HashMap<>();
// 判断token是否为null
Claims claims = Jwts.parser().setSigningKey("my-123").parseClaimsJws(token).getBody();
Integer id=Integer.valueOf(claims.getId());
System.out.println("用户时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"). format(claims.getIssuedAt()));
System.out.println("过期时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"). format(claims.getExpiration()));
String username=claims.getSubject();
User user=userService.querybyid(id);
f(username!=null&&claims.getId()!=null&&username.equals(user.getUsername())){
String gettoken=CreateJwt.getoken(user);
map.put("user",user);
map.put("token",token);
return map;
访问权限进行控制
应用的安全性包括用户认证(Authentication)
和用户授权(Authorization)
两个部分。
用户认证:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码,系统通过校验用户名和密码来完成认证过程。
用户授权:验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。
<!-- spring security 安全认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
启动日志增加了如下内容,通过该内容可以找到默认用户名
和密码
security JSESSIONID 登录后的用户信息默认在 Cookies
自定义登录认证逻辑
@Slf4j @Component @RequiredArgsConstructor
![c853babc4dfea3cba57af2f32da55855.png](https://i-blog.csdnimg.cn/blog_migrate/5264d3d2c9f841cb407e3f0a888827d4.png)
不建议使用lombok
UserDetailsServiceImpl
用户验证处理
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
LoginFailureHandler
// 账号过期
log.info("[登录失败] - 用户账号过期");
log.info("[登录失败] - 用户密码错误");
log.info("[登录失败] - 用户密码过期");
log.info("[登录失败] - 用户被禁用");
log.info("[登录失败] - 用户被锁定");
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
SecurityConfig
WebSecurityConfig
/** 登录成功的处理 */
private final LoginSuccessHandler loginSuccessHandler;
/** 登录失败的处理 */
private final LoginFailureHandler loginFailureHandler;
/** 配置认证方式等 */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(mingYueUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
/** http相关的配置,包括登入登出、异常处理、会话管理等 */
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable();
http.authorizeRequests()
// 放行接口
// .antMatchers().permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest()
.authenticated()
// 登入
.and()
.formLogin()
// 允许所有用户
.permitAll()
// 登录成功处理逻辑
.successHandler(loginSuccessHandler)
// 登录失败处理逻辑
.failureHandler(loginFailureHandler);
}
<!-- redis 缓存操作 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
<!-- redis 缓存操作 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- pool 对象池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
RedisCache
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value);
}
![86be62b6dd8c490edf7b4b646f09ab05.png](https://i-blog.csdnimg.cn/blog_migrate/6a366979c9ebb09f1bb3a89654079427.png)
![89baa6aa7b9b07612be987d4bba7cee3.png](https://i-blog.csdnimg.cn/blog_migrate/37fa562d6c50766c1eb0106ae4578bfc.png)
![b3963573732d352805bee30a1038b9e8.png](https://i-blog.csdnimg.cn/blog_migrate/9543073f2a5067ca827144d7035e6e97.png)
![0d5c4ac2383609f8f2168d4ff7c6d9c1.png](https://i-blog.csdnimg.cn/blog_migrate/23d20fd6ffa219d094fd9db234f44878.png)
GenericToStringSerializer: 可以将任何对象泛化为字符串并序列化
Jackson2JsonRedisSerializer: 跟JacksonJsonRedisSerializer实际上是一样的
JacksonJsonRedisSerializer: 序列化object对象为json字符串
JdkSerializationRedisSerializer: 序列化java对象
StringRedisSerializer: 简单的字符串序列化
我们可以根据redis操作的不同数据类型,设置对应的序列化方式。
默认使用的是JdkSerializationRedisSerializer. 这种序列化最大的问题就是存入对象后,我们很难直观看到存储的内容,很不方便我们排查问题
而一般我们最经常使用的对象序列化方式是:Jackson2JsonRedisSerializer
RedisConfig
![6c4c713ff5052ef3951e405bd934f7bf.png](https://i-blog.csdnimg.cn/blog_migrate/f50407cb4d1377b434a935b20a4bab94.png)
可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行
这把锁要是一把可重入锁(避免死锁)
这把锁最好是一把阻塞锁
有高可用的获取锁和释放锁功能
获取锁和释放锁的性能要好
分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。
互斥性。在任意时刻,只有一个客户端能持有锁。
不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
登录注册
import { validUsername, isNumber } from '@/utils/validate'
export default {
name: 'Login',
data() {
const validatePhoneNo = (rule, value, callback) => {
if (!(value.length === 11 && isNumber(value))) {
callback(new Error('手机号码必须是11位数字'))
} else {
if (value.charAt(0) !== '1' || parseInt(value.charAt(1)) < 3) {
callback(new Error('输入的手机号码不是有效的手机号'))
} else {
callback()
}
}
}
const validatePhoneCode = (rule, value, callback) => {
if (!value) {
callback(new Error('验证码不能为空'))
} else {
if (!(value.length === 6 && isNumber(value))) {
callback(new Error('验证码必须是6位数字'))
} else {
callback()
}
}
}
}
return {
loginRules: {
username: [{ required: true, trigger: 'blur', validator: validateUsername }],
password: [{ required: true, trigger: 'blur', validator: validatePassword }],
phoneNo: [{ required: true, trigger: 'blur', validator: validatePhoneNo }],
phoneCode: [{ required: true, trigger: 'blur', validator: validatePhoneCode }]
}
// 其他返回对象在此省略
}
}
/**
* 是否数字
* @param {String} val
* @returns {Boolean}
*/
export function isNumber(val) {
for (let i = 0; i < val.length; i++) {
if (val.charCodeAt(i) < 48 || val.charCodeAt(i) > 57) {
return false
}
}
return true
}
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" autocomplete="on" label-position="left">
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true
if (this.activeLoginType === '1') {
const username = this.loginForm.username
const password = this.loginForm.password
this.$store.dispatch('user/login', { username: username, password: password })
.then(() => {
this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
this.loading = false
})
} else {
const phoneNo = this.loginForm.phoneNo
const phoneCode = this.loginForm.phoneCode
this.$store.dispatch('user/mobileLogin', { phoneNo: phoneNo, phoneCode: phoneCode })
.then(() => {
this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
this.loading = false
})
}
}
})
}
import { login, logout, phoneCodeLogin } from '@/api/user'
const actions = {
// user login
login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({ username: username, password: password }).then(response => {
if (response.status === 200 && response.data) {
const data = response.data.userInfo
const useBaseInfo = {
username: data.username,
nickname: data.nickname,
email: data.email,
phoneNum: data.phoneNum
}
window.sessionStorage.setItem('userInfo', JSON.stringify(useBaseInfo))
const { roles, currentRole } = data
roles[0] = currentRole
commit('SET_TOKEN', useBaseInfo)
commit('SET_NAME', useBaseInfo.username)
setToken(currentRole.id)
commit('SET_ROLES', roles)
window.sessionStorage.setItem('roles', JSON.stringify(roles))
commit('SET_CURRENT_ROLE', currentRole)
window.sessionStorage.setItem('currentRole', currentRole)
// commit('SET_AVATAR', avtar)
getRouteIds(currentRole.id).then(response => {
if (response.status === 200 && response.data.status === 200) {
const routeIds = response.data['data']
window.sessionStorage.setItem('routeData', JSON.stringify(routeIds))
} else {
Message.error('response.status=' + response.status + 'response.text=' + response.text)
}
})
resolve(useBaseInfo)
} else {
Message.error('user login failed')
resolve()
}
}).catch(error => {
console.error(error)
reject(error)
})
})
},
// phone code login
mobileLogin({ commit }, phoneParam) {
const { phoneNo, phoneCode } = phoneParam
return new Promise((resolve, reject) => {
phoneCodeLogin({ phoneNo: phoneNo, phoneCode: phoneCode }).then(res => {
if (res.status === 200 && res.data) {
const data = res.data.userInfo
const useBaseInfo = {
username: data.username,
nickname: data.nickname,
phoneNum: data.phoneNum,
email: data.email
}
window.sessionStorage.setItem('userInfo', JSON.stringify(useBaseInfo))
const { roles, currentRole } = data
roles[0] = currentRole
commit('SET_TOKEN', useBaseInfo)
commit('SET_NAME', useBaseInfo.username)
setToken(currentRole.id)
commit('SET_ROLES', roles)
window.sessionStorage.setItem('roles', JSON.stringify(roles))
commit('SET_CURRENT_ROLE', currentRole)
window.sessionStorage.setItem('currentRole', currentRole)
// commit('SET_AVATAR', avtar)
getRouteIds(currentRole.id).then(response => {
if (response.status === 200 && response.data.status === 200) {
const routeIds = response.data['data']
window.sessionStorage.setItem('routeData', JSON.stringify(routeIds))
} else {
Message.error('response.status=' + response.status + 'response.text=' + response.text)
}
})
resolve(useBaseInfo)
} else {
Message.error('phone code login failed')
resolve()
}
}).catch(error => {
console.error(error)
reject(error)
})
})
},
// 其他请求此处省略
}
可以在 jwt.io Debugger[1] 网站来解码、验证和生成 JWT。
由于缺乏安全性,不应该将敏感的会话数据存储在浏览器中。每当用户需要访问受保护的路由或资源时,用户代理应该发送jwt,通常在 Authorization header 中使用 Bearer 模式。
<!--加解密依赖-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
<!--持久层框架mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.2</version>
</dependency>
<!--spring security依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>
<!--jwt token依赖-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.7.0</version>
</dependency>
public class JwtTokenUtil {
// 密钥
private static final String SECRET = "bonusBACKEND2022$";
// 过期时间7天
private static final int EXPIRE_SECONDS = 7*24*3600;
private final static Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);
/**
* 生成token方法
* @param memInfoMap
* @return jwtToken
*/
public static String genAuthenticatedToken(Map<String, Object> memInfoMap){
List<GrantedAuthority> authorities = (List<GrantedAuthority>) memInfoMap.get("authorities");
String authorityStr = null;
if(authorities!=null && authorities.size()>0){
StringBuffer buffer = new StringBuffer();
for(int i=0; i<authorities.size()-1; i++){
buffer.append(authorities.get(i).getAuthority()).append(",");
}
buffer.append(authorities.get(authorities.size()-1).getAuthority());
authorityStr = buffer.toString();
}
String[] authorityArray = authorityStr!=null?authorityStr.split(","):null;
Calendar nowTime = Calendar.getInstance();
//过期时间
nowTime.add(Calendar.SECOND, EXPIRE_SECONDS);
Date expireDate = nowTime.getTime();
String jwtToken = JWT.create().withJWTId(UUID.randomUUID().toString().replaceAll("-", ""))
.withClaim("memId", (Long) memInfoMap.get("memId"))
.withClaim("memAccount", (String) memInfoMap.get("memAccount"))
.withClaim("memPwd", (String) memInfoMap.get("memPwd"))
.withClaim("totalCreditAmount", ((BigDecimal) memInfoMap.get("totalCreditAmount")).doubleValue())
.withClaim("usedCreditAmount", ((BigDecimal) memInfoMap.get("usedCreditAmount")).doubleValue())
.withClaim("remainCreditAmount", ((BigDecimal) memInfoMap.get("remainCreditAmount")).doubleValue())
.withArrayClaim("authorities", authorityArray)
.withIssuedAt(new Date(System.currentTimeMillis()))
.withExpiresAt(expireDate)
.sign(Algorithm.HMAC256(SECRET));
return jwtToken;
}
}
JwtTokenUtil的工具类用于生成jwt令牌
public class JwtTokenUtil {
// 密钥
private static final String SECRET = "bonusBACKEND2022$";
// 过期时间7天
private static final int EXPIRE_SECONDS = 7*24*3600;
private final static Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);
/**
* 生成token方法
* @param memInfoMap
* @return jwtToken
*/
public static String genAuthenticatedToken(Map<String, Object> memInfoMap){
List<GrantedAuthority> authorities = (List<GrantedAuthority>) memInfoMap.get("authorities");
String authorityStr = null;
if(authorities!=null && authorities.size()>0){
StringBuffer buffer = new StringBuffer();
for(int i=0; i<authorities.size()-1; i++){
buffer.append(authorities.get(i).getAuthority()).append(",");
}
buffer.append(authorities.get(authorities.size()-1).getAuthority());
authorityStr = buffer.toString();
}
String[] authorityArray = authorityStr!=null?authorityStr.split(","):null;
Calendar nowTime = Calendar.getInstance();
//过期时间
nowTime.add(Calendar.SECOND, EXPIRE_SECONDS);
Date expireDate = nowTime.getTime();
String jwtToken = JWT.create().withJWTId(UUID.randomUUID().toString().replaceAll("-", ""))
.withClaim("memId", (Long) memInfoMap.get("memId"))
.withClaim("memAccount", (String) memInfoMap.get("memAccount"))
.withClaim("memPwd", (String) memInfoMap.get("memPwd"))
.withClaim("totalCreditAmount", ((BigDecimal) memInfoMap.get("totalCreditAmount")).doubleValue())
.withClaim("usedCreditAmount", ((BigDecimal) memInfoMap.get("usedCreditAmount")).doubleValue())
.withClaim("remainCreditAmount", ((BigDecimal) memInfoMap.get("remainCreditAmount")).doubleValue())
.withArrayClaim("authorities", authorityArray)
.withIssuedAt(new Date(System.currentTimeMillis()))
.withExpiresAt(expireDate)
.sign(Algorithm.HMAC256(SECRET));
return jwtToken;
}
}
实现用户认证方法
@Service
public class MemInfoServiceImpl extends ServiceImpl<MemInfoMapper, MemInfoDTO> implements MemInfoService {
private final static Logger logger = LoggerFactory.getLogger(MemInfoServiceImpl.class);
@Resource
private MyPasswordEncoder passwordEncoder;
@Resource
private RoleInfoService roleInfoService;
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MemInfoDTO memInfoDTO = this.baseMapper.getMemInfoByAccount(username);
if(memInfoDTO==null){
throw new UsernameNotFoundException("Username" + username + "is invalid!");
}
// 获取用户角色列表
List<RoleInfoDTO> roleInfoDTOList = roleInfoService.getRolesByMemId(memInfoDTO.getMemId());
if(roleInfoDTOList.size()>0){
for(RoleInfoDTO roleInfoDTO: roleInfoDTOList){
SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_" + roleInfoDTO.getRoleName().toUpperCase());
memInfoDTO.getAuthorities().add(grantedAuthority);
}
}
return memInfoDTO;
}
@Data
@TableName("bonus_mem_info")
@ApiModel(value="MemInfoDTO", description = "会员DTO")
@Validated
public class MemInfoDTO extends BaseDTO implements UserDetails {
/**
* 会员id
*/
@TableId
@ApiModelProperty(name = "memId", value = "memId", notes = "会员ID", dataType = "Long")
private Long memId;
/**
* 会员账号
*/
@TableField(value = "mem_account")
@NotEmpty(message = "会员账号不能为空")
@ApiModelProperty(name="memAccount", value = "memAccount", notes = "会员账号", dataType = "String")
private String memAccount;
/**
* 会员密码
*/
@TableField(value = "mem_pwd")
@NotEmpty(message = "会员密码不能为空")
@ApiModelProperty(name="memPwd", value = "memPwd", notes = "加密后的会员密码", dataType = "String")
private String memPwd;
/**
* 会员类型:1-vip;2-代理
*/
@TableField(value = "mem_type")
@NotEmpty(message = "会员类型不能为空")
@ApiModelProperty(name="memType", value = "memType", notes = "会员类型", dataType = "Integer", example = "1", allowableValues = "1,2")
private Integer memType;
/**
* 会员信用额度,单位分
*/
@TableField(value = "total_credit_amount")
@NotEmpty(message = "会员信用额度不能为空")
@ApiModelProperty(name = "totalCreditAmount", value = "totalCreditAmount", notes = "会员总信用额度,单位分", dataType = "Long", example = "10000")
private Long totalCreditAmount;
/**
* 会员已使用信用额度,单位分
*/
@ApiModelProperty(name = "usedCreditAmount", value = "usedCreditAmount", notes = "会员已使用信用额度,单位分", dataType = "Long", example = "5000")
@TableField(value = "used_credit_amount")
private Long usedCreditAmount;
@TableField(exist = false)
private List<GrantedAuthority> authorities = new ArrayList<>();
@Override
public Collection<GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return this.memPwd;
}
@Override
public String getUsername() {
return this.memAccount;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final static Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
@Resource
private MemInfoService memInfoService;
private MathContext mathContext = new MathContext(2, RoundingMode.HALF_UP);
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
auth.userDetailsService(memInfoService);
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/static/**","/index.html","/templates/**", "/admin/**", "/doc.html", "/webjars/**", "/v2/*", "/favicon.ico", "/swagger-resources");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
JwtAuthenticationFilterBean jwtAuthenticationFilterBean = new JwtAuthenticationFilterBean();
http.addFilterBefore(jwtAuthenticationFilterBean, UsernamePasswordAuthenticationFilter.class); // 将JwtToken认证过滤器注册在登录认证过滤器之前
// 配置跨域
http.cors().configurationSource(corsConfigurationSource())
.and().logout().invalidateHttpSession(true).logoutUrl("/member/logout").permitAll()
;
http.authorizeRequests().antMatchers("/member/checkSafetyCode").permitAll()
.antMatchers("/doc.html").permitAll()
.antMatchers("/common/kaptcha").permitAll()
.antMatchers("/admin/login").permitAll()
.anyRequest().authenticated()
.and().httpBasic()
.and().formLogin()
.loginProcessingUrl("/member/login") // 登录接口
.successHandler((httpServletRequest, httpServletResponse, authentication) -> {
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.setStatus(HttpStatus.OK.value());
PrintWriter printWriter = httpServletResponse.getWriter();
MemInfoDTO memInfoDTO = (MemInfoDTO) authentication.getPrincipal();
Map<String, Object> userMap = new HashMap<>();
userMap.put("memId", memInfoDTO.getMemId());
userMap.put("memAccount", memInfoDTO.getMemAccount());
userMap.put("memPwd", memInfoDTO.getMemPwd());
BigDecimal totalCredit = memInfoDTO.getTotalCreditAmount()!=null?new BigDecimal(memInfoDTO.getTotalCreditAmount()/100, mathContext): new BigDecimal("0.0");
userMap.put("totalCreditAmount", totalCredit);
BigDecimal usedCredit = memInfoDTO.getUsedCreditAmount()!=null?new BigDecimal(memInfoDTO.getUsedCreditAmount()/100, mathContext):new BigDecimal("0.0");
userMap.put("usedCreditAmount", usedCredit);
Long remainCredit = (memInfoDTO.getTotalCreditAmount()==null?0:memInfoDTO.getTotalCreditAmount()) - (memInfoDTO.getUsedCreditAmount()==null?0:memInfoDTO.getUsedCreditAmount());
BigDecimal remainCreditAmount = new BigDecimal(remainCredit/100, mathContext);
userMap.put("remainCreditAmount", remainCreditAmount);
userMap.put("authorities", memInfoDTO.getAuthorities());
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("memInfo", userMap);
dataMap.put("authenticatedToken", "Bearer "+JwtTokenUtil.genAuthenticatedToken(userMap));
ResponseResult<Map<String, Object>> responseResult = ResponseResult.success(dataMap, "login success");
printWriter.write(JSONObject.toJSONString(responseResult));
printWriter.flush();
printWriter.close();
}).failureHandler((httpServletRequest, httpServletResponse, e) -> {
logger.error("login failed, caused by " + e.getMessage());
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
httpServletResponse.setStatus(HttpStatus.OK.value());
PrintWriter printWriter = httpServletResponse.getWriter();
ResponseResult<String> responseResult = ResponseResult.error(HttpStatus.UNAUTHORIZED.value(), "authentication failed");
responseResult.setPath(httpServletRequest.getRequestURI());
printWriter.write(JSONObject.toJSONString(responseResult));
printWriter.flush();
printWriter.close();
}).permitAll()
.and().csrf().disable().exceptionHandling().accessDeniedHandler(accessDeniedHandler());
}
//配置跨域访问资源
private CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*"); //同源配置,*表示任何请求都视为同源,若需指定ip和端口可以改为如“localhost:8080”,多个以“,”分隔;
corsConfiguration.addAllowedHeader("*");//header,允许哪些header,本案中使用的是token,此处可将*替换为token;
corsConfiguration.addAllowedMethod("*"); //允许的请求方法,PSOT、GET等
corsConfiguration.setAllowCredentials(true);
// 注册跨域配置
source.registerCorsConfiguration("/**",corsConfiguration); //配置允许跨域访问的url
return source;
}
@Bean
AccessDeniedHandler accessDeniedHandler() {
return new AuthenticationAccessDeniedHandler();
}
}
![e0dc156aa1b091fc0e63e2d055ec8260.png](https://i-blog.csdnimg.cn/blog_migrate/bf5e4f6bfcca5fd37f1d540b7e6ee525.png)
加群联系作者vx:xiaoda0423
仓库地址:https://github.com/webVueBlog/JavaGuideInterview