十四、spring security 集成 jpa + mysql

引入相关依赖

  • 加入mysql数据库连接
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.19</version>
        </dependency>
  • 加入数据库jdbc连接
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

准备好数据库表, 以及初始化数据

CREATE TABLE IF NOT EXISTS mooc_roles (
  id BIGINT NOT NULL AUTO_INCREMENT,
  role_name VARCHAR(50) NOT NULL,
  PRIMARY KEY (id),
  CONSTRAINT uk_mooc_roles_role_name UNIQUE (role_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE IF NOT EXISTS mooc_users (
  id BIGINT NOT NULL AUTO_INCREMENT,
  account_non_expired BIT NOT NULL,
  account_non_locked BIT NOT NULL,
  credentials_non_expired BIT NOT NULL,
  email VARCHAR(254) NOT NULL,
  enabled BIT NOT NULL,
  mobile VARCHAR(11) NOT NULL,
  name VARCHAR(50) NOT NULL,
  password_hash VARCHAR(80) NOT NULL,
  username VARCHAR(50) NOT NULL,
  PRIMARY KEY (id),
  CONSTRAINT uk_mooc_users_username UNIQUE (username),
  CONSTRAINT uk_mooc_users_mobile UNIQUE (mobile),
  CONSTRAINT uk_mooc_users_email UNIQUE (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE IF NOT EXISTS mooc_users_roles (
  user_id BIGINT NOT NULL,
  role_id BIGINT NOT NULL,
  PRIMARY KEY (user_id, role_id),
  CONSTRAINT fk_users_roles_user_id_mooc_users_id FOREIGN KEY (user_id) REFERENCES mooc_users (id),
  CONSTRAINT fk_users_roles_role_id_mooc_roles_id FOREIGN KEY (role_id) REFERENCES mooc_roles (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

insert into mooc_users(id, username, `name`, mobile, password_hash, enabled, account_non_expired, account_non_locked, credentials_non_expired, email)
values (1, 'user', 'zhnagsan', '13000000001', '{bcrypt}$2a$10$BFtL.Gx1qoIX1YmdCM0jwe8/DWCZ8yCX2ut.bc3y3I4.ab8ork1NC', 1, 1, 1, 1, 'zhangsan@local.dev'),
       (2, 'old_user', 'lisi', '13000000002', '{SHA-1}85a4f70357bd47d7a76cac02b7584045482fd9b0', 1, 1, 1, 1, 'lisi@local.dev');
insert into mooc_roles(id, role_name) values (1, 'ROLE_USER'), (2, 'ROLE_ADMIN');
insert into mooc_users_roles(user_id, role_id) values (1, 1), (1, 2), (2, 1);

添加yml配置

#spring.security.user.name=user
#spring.security.user.password=12345
#spring.security.user.roles=ADMIN,USER
logging:
  level:
    com:
      example.example.uaa: DEBUG
    org:
      springframework:
        security: DEBUG
        jdbc: TRACE
spring:
  # H2数据源
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 数据库连接url
    # MODE=MySQL:兼容MySQL写法
    # DATABASE_TO_LOWER=TRUE :表名转小写
    # CASE_INSENSITIVE_IDENTIFIERS=TRUE : 不区分大小写
    # DB_CLOSE_DELAY=-1 : 不自动关闭数据库连接
    url: jdbc:mysql://192.168.0.101:3306/uaa?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8
    username: aesop
    password: aesop
  jpa:
    hibernate:
      # 官方说明: https://docs.spring.io/spring-boot/docs/2.7.12/reference/html/howto.html#howto.data-initialization.using-jpa
      ddl-auto: none
    database-platform: org.hibernate.dialect.MySQL8Dialect
    database: MySQL
    # 官方说明:https://docs.spring.io/spring-boot/docs/2.7.12/reference/html/howto.html#howto.data-initialization
    # 这将推迟数据源初始化,直到创建和初始化任何 EntityManagerFactory beans 之后
    # 解决了data.sql文件执行报错的问题,原理就是创建好表实体产生表结构后,再执行初始化
    defer-datasource-initialization: true

SecurityConfig 核心配置类

package com.example.uaa.config;

import com.example.uaa.dao.UserRepo;
import com.example.uaa.filter.RestAuthenticationFilter;
import com.example.uaa.service.impl.UserDetailServiceImpl;
import com.example.uaa.service.impl.UserDetailsPasswordServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.passay.MessageResolver;
import org.passay.spring.SpringMessageResolver;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.MessageDigestPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

import javax.sql.DataSource;
import java.util.Map;

/**
 * core配置
 */
@Slf4j
@Configuration
@EnableWebSecurity(debug = false) // 开启调试模式,生产环境不要使用
@RequiredArgsConstructor
public class SecurityConfig {
    private final ObjectMapper objectMapper;
    private final AuthenticationConfiguration authenticationConfiguration;
    private final MessageSource messageSource;

    // private final DataSource dataSource;

    private final UserRepo userRepo;

    // @Bean
    // public DataSource dataSource() {
    //     return new EmbeddedDatabaseBuilder()
    //             .setType(EmbeddedDatabaseType.H2)
    //             .addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION)// 自动建表
    //             .build();
    // }

    /**
     * 注册passay的国际化消息处理类
     *
     * @return
     */
    @Bean
    public MessageResolver messageResolver() {
        return new SpringMessageResolver(messageSource);
    }

    /**
     * 自定义验证注解,添加国际化消息处理类
     * @return
     */
    @Bean
    public LocalValidatorFactoryBean localValidatorFactoryBean() {
        LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
        bean.setValidationMessageSource(messageSource);
        return bean;
    }


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http
                .authorizeHttpRequests((req) -> req
                        // 需要认证并拥有特定角色才能访问的URL
                        .antMatchers("/api/**").hasRole("USER")
                        .antMatchers("/admin/**").hasRole("ADMIN")
                        // 放行不需要认证的URL
                        .antMatchers("/authorize/**").permitAll()
                        .antMatchers("/webjars/**", "/error", "/h2-console/**").permitAll()
                        // 放行swagger3
                        .antMatchers("/swagger-ui/**", "/swagger-resources/**", "/v3/**", "/doc.html").permitAll()
                        // 放行通用的css、js等静态资源位置
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                        // 除了以上规则,默认拦截所有URL请求,要求认证后才能访问(该语句必须要放在末尾)
                        .anyRequest().authenticated()
                )
                // 用于配置H2控制台界面的正常显示 https://docs.spring.io/spring-boot/docs/2.7.11/reference/html/data.html#data.sql.h2-web-console.spring-security
                .headers((headers) -> headers.frameOptions().sameOrigin())
                // 添加自定义登录过滤器,替换掉默认的UsernamePasswordAuthenticationFilter过滤器
                .addFilterAt(restAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                // 以下表单登录,被restAuthenticationFilter配置替换:
                // .formLogin(form-> form
                //         // 定义登陆页面
                //         .loginPage("/login").permitAll()
                //         // 登录成功后跳转的页面
                //         // .defaultSuccessUrl("/")
                //         // 登录成功后置处理器。会覆盖掉defaultSuccessUrl的配置
                //         .successHandler(((request, response, authentication) -> {
                //             ObjectMapper objectMapper = new ObjectMapper();
                //             response.setStatus(HttpStatus.OK.value());
                //             response.getWriter().println("LoginSuccess:"+
                //                     objectMapper.writeValueAsString(authentication));
                //         }))
                //         // 登录失败后置处理器,不配置,默认跳转/login?error
                //         .failureHandler(((request, response, exception) -> {
                //             response.setStatus(HttpStatus.UNAUTHORIZED.value());
                //             response.getWriter().println("LoginFailure!");
                //         }))
                // )
                // 配置退出登录的路径,配置后使用POST请求。若不配置,默认为/logout,退出成功跳到/login
                // .logout(logout-> logout.logoutUrl("/logout").logoutSuccessUrl("/login"))
                .httpBasic(Customizer.withDefaults())
                // 开启CSRF ,并配置不需要CSRF的路径
                // .csrf(csrf->csrf.ignoringAntMatchers("/api/**"))
                // .csrf()
                // 禁用csrf
                .csrf().disable()
                .rememberMe(Customizer.withDefaults());
        // ...
        return http.build();
    }

    /**
     * 设定认证方式 Authentication
     * 代替了配置
     * spring.security.user.name=user
     * spring.security.user.password=12345
     * spring.security.user.roles=ADMIN,USER
     *
     * 基于spring boot 2.7 后新的认证方式:
     * https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
     */

    /**
     * 基于内存的认证 InMemoryUserDetailsManager
     */
    // @Bean
    // public InMemoryUserDetailsManager userDetailsService() {
    //     UserDetails user = User.builder()
    //             .username("user")
    //             .password(passwordEncoder().encode("123456"))
    //             .roles("USER")
    //             .build();
    //     return new InMemoryUserDetailsManager(user);
    // }

    /**
     * 基于JDBC的连接H2
     * @return
     */
    // @Bean
    // public UserDetailsManager userDetailsManager() {
    //     UserDetails user = User.builder()
    //             .username("user")
    //             .password(passwordEncoder().encode("123456"))
    //             .roles("USER")
    //             .build();
    //     JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource());
    //     users.createUser(user);
    //     return users;
    // }

    @Bean
    public UserDetailsService userDetails() {
        return new UserDetailServiceImpl(userRepo);
    }
    @Bean
    public UserDetailsPasswordService userDetailsPasswordService() {
        return new UserDetailsPasswordServiceImpl(userRepo);
    }

    /**
     * 使用BCrypt密码加密类型,这里还可以增加其他类型,如果增加其他类型,表示对其他的加密类型进行兼容处理,而新的加密则默认使用的BCryptPasswordEncoder加密算法。
     *
     * 简单地理解为,遇到新密码,DelegatingPasswordEncoder会委托给BCryptPasswordEncoder(encodingId为bcryp*)进行加密,同时,
     * 对历史上使用ldap,MD4,MD5等等加密算法的密码认证保持兼容(如果数据库里的密码使用的是MD5算法,那使用matches方法认证仍可以通过,但新密码会使bcrypt进行储存),
     *
     * 支持的一些加密类型{@link PasswordEncoderFactories}
     * ————————————————
     * 原文链接:https://blog.csdn.net/alinyua/article/details/80219500
     * @return PasswordEncoder
     */
    @Bean
    static PasswordEncoder passwordEncoder() {
        // 数据库存储格式,{bcrypt}xxxx ,例如 '{bcrypt}$2a$10$hpROwWyEu4AbMJpFF2jLBusGdRnOK2VktGy3VVnrsmkC98TFWnJ.K'
        val idForEncode = "bcrypt";
        return new DelegatingPasswordEncoder(idForEncode,Map.of(
                idForEncode, new BCryptPasswordEncoder(),
                "SHA-1", new MessageDigestPasswordEncoder("SHA-1")));
        // return new BCryptPasswordEncoder();
    }

    /**
     * 生成加密密码demo
     * @param args
     */
     public static void main(String[] args) {
         //System.out.println(new BCryptPasswordEncoder().encode("123456"));
         System.out.println(new MessageDigestPasswordEncoder("SHA-1").encode("123456"));
     }

    /**
     * 设置登录地址入口:http://localhost:8080/authorize/login
     * 请求方式POST application/json
     * @return
     * @throws Exception
     */
    private RestAuthenticationFilter restAuthenticationFilter() throws Exception {
        RestAuthenticationFilter filter = new RestAuthenticationFilter(objectMapper);
        // 登录成功后置处理器。
        filter.setAuthenticationSuccessHandler((request, response, authentication) -> {
            ObjectMapper objectMapper = new ObjectMapper();
            response.setStatus(HttpStatus.OK.value());
            response.getWriter().println("LoginSuccess: "+
                    objectMapper.writeValueAsString(authentication));
        });
        // 登录失败后置处理器
        filter.setAuthenticationFailureHandler((request, response, exception) -> {
            log.error("认证失败", exception);
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.getWriter().println("LoginFailure!");
        });
        // 获得AuthenticationManager
        filter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
        // 设置登登录请求地址
        filter.setFilterProcessesUrl("/authorize/login");
        return filter;
    }

    // /**
    //  * 放行不需要认证的URL
    //  * 另一种放行方法是在HttpSecurity 对url添加permitAll()放行(官方推荐)
    //  * @return
    //  */
    // @Bean
    // public WebSecurityCustomizer webSecurityCustomizer() {
    //     return (web) -> web.ignoring().antMatchers("/api/say")
    //             // 放行通用的css、js等静态资源位置
    //             .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    // }
}

WebMvcConfig 配置

package com.example.uaa.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.webjars.WebJarAssetLocator;

import java.util.Map;

/**
 * 前端资源配置
 */
@RequiredArgsConstructor
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    //D:\Code\apache-maven-3.6.3\repo\org\webjars\bootstrap\4.5.0\bootstrap-4.5.0.jar!\META-INF\resources\webjars\bootstrap\4.5.0\js\bootstrap.js
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {




        registry.setOrder(1);
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // “/login”对应访问的登录页URL, “login”对应login.html具体的视图
        registry.addViewController("/login").setViewName("login");
        registry.addViewController("/").setViewName("index");
        // swagger 地址
        // registry.addViewController( "/swagger-ui/")
        //         .setViewName("forward:/swagger-ui/index.html");
        // 优先级最高
        registry.setOrder(Ordered.HIGHEST_PRECEDENCE);

    }
}

身份认证过滤器RestAuthenticationFilter

package com.example.uaa.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 自定义登录过滤器,使用json格式传参数。返回json数据
 */
@RequiredArgsConstructor
public class RestAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final ObjectMapper objectMapper;

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("不支持的请求方法: " + request.getMethod());
        }
        UsernamePasswordAuthenticationToken authRequest;
        try {
            var jsonNode = objectMapper.readTree(request.getInputStream());
            String username = jsonNode.get("username").textValue();
            String password = jsonNode.get("password").textValue();

            authRequest = new UsernamePasswordAuthenticationToken(username, password);

        } catch (IOException e) {
            e.printStackTrace();
            throw new BadCredentialsException("参数解析出错!");
        }

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        // AuthenticationManager 与 UserDetailService之间的关,以及如何使用UserDetailService来完成认证请看:
        // https://docs.spring.io/spring-security/reference/5.7/servlet/authentication/passwords/dao-authentication-provider.html
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

posted @ 2023-10-06 20:22  aesopcmc  阅读(59)  评论(0)    收藏  举报