springboot第22集:security,Lombok,token,redis

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常用组件外,还有以下一些组件:

  1. AccessDecisionManager:用于根据用户和资源的相关信息判断是否允许用户访问资源。

  2. AuthenticationEntryPoint:如果一个未认证的用户试图访问需要认证的资源,会被重定向到该接口实现的方法处理。

  3. AuthenticationProvider:用于对用户进行认证并生成认证对象 Authentication。

  4. FilterSecurityInterceptor:在请求到达后台之前进行拦截和处理,包含很多安全检查点。

  5. RememberMeAuthenticationProvider:为支持“记住我”功能提供的认证处理器,用于生成认证对象 Authentication。

  6. 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

image.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

image.png
2a7db92a6128691f18fe885554b9a0ab.png
image.png
700a5a5614b26c1ba0a2a8f95e68d12d.png
image.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邮箱

[email protected]

#发送邮箱的密码

spring.mail.password=123

qq登录:

设置->账户->POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务

d5359ec266ed7efd291df32ebccd2deb.png

image.png

微信登录QQ邮箱:

个人头像->设置->第三方服务

安装Redis

下载Redis

下载地址:https://wwur.lanzout.com/iD8Ow0ti96dg 密码:4y2e

安装

解压直接双击安装,无需修改配置,一路下一步即可

a74ae953de31f126ec929bfb79fcbac7.png

安装ffmpeg

下载

下载地址:https://wwur.lanzout.com/iORvc0tia6uj 密码:9n15

查看版本

然后开启doc执行ffmpeg -verison

搜索gitlab镜像

由于Mac M1芯片区别去Intel,所以在找镜像的时候需要勾选ARM 64,然后一般推荐的镜像就是gitlab-ce。

8b3dabcecf3ca27204c2a7eafb9d28cb.png

image.png
7e305478366016ab7bc67ea0886adbad.png
image.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

image.png
99c838783b2abdb9da33a87f25426695.png
image.png
<!--jsonwebtoken 生成token的库 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
TokenService
createToken
parseToken

c5bf1ccd8fc4bfc9361bc2164cc9711f.png

image.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

image.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

image.png
89baa6aa7b9b07612be987d4bba7cee3.png
image.png
b3963573732d352805bee30a1038b9e8.png
image.png
0d5c4ac2383609f8f2168d4ff7c6d9c1.png
image.png
  • GenericToStringSerializer: 可以将任何对象泛化为字符串并序列化

  • Jackson2JsonRedisSerializer: 跟JacksonJsonRedisSerializer实际上是一样的

  • JacksonJsonRedisSerializer: 序列化object对象为json字符串

  • JdkSerializationRedisSerializer: 序列化java对象

  • StringRedisSerializer: 简单的字符串序列化

我们可以根据redis操作的不同数据类型,设置对应的序列化方式。

默认使用的是JdkSerializationRedisSerializer. 这种序列化最大的问题就是存入对象后,我们很难直观看到存储的内容,很不方便我们排查问题

而一般我们最经常使用的对象序列化方式是:Jackson2JsonRedisSerializer

RedisConfig

6c4c713ff5052ef3951e405bd934f7bf.png

image.png

可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行
这把锁要是一把可重入锁(避免死锁)
这把锁最好是一把阻塞锁
有高可用的获取锁和释放锁功能
获取锁和释放锁的性能要好

分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。

  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

  3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。

  4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

登录注册

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

image.png

加群联系作者vx:xiaoda0423

仓库地址:https://github.com/webVueBlog/JavaGuideInterview

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>