Loading

SpringMVC整合MyBatis

扯dz

内容包括

  1. SpringMVC整合MyBatis
  2. Druid数据源
  3. MyBatis TypeHandler和Spring Converter
  4. Thymeleaf视图
  5. Spring Security进行安全保护
  6. 用户注册登录

就是一个博客应用的简单的登录注册系统

库版本信息

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.target>1.8</maven.compiler.target>
    <maven.compiler.source>1.8</maven.compiler.source>
    <junit.version>5.7.1</junit.version>
    <spring.framework.version>5.3.10</spring.framework.version>
    <spring.security.version>5.5.2</spring.security.version>
    <mybatis.spring.version>2.0.6</mybatis.spring.version>
    <mybatis.version>3.5.1</mybatis.version>
    <log4j.version>1.2.17</log4j.version>
    <mysql.connector.version>8.0.26</mysql.connector.version>
    <druid.version>1.2.6</druid.version>
    <thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
    <hibernate.validator.version>6.0.21.Final</hibernate.validator.version>
</properties>

SpringMVC整合MyBatis

引入依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>${spring.framework.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>${spring.framework.version}</version>
</dependency>


<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>${mybatis.spring.version}</version>
</dependency>

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>${mybatis.version}</version>
</dependency>


<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>${mysql.connector.version}</version>
</dependency>


<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>${druid.version}</version>
</dependency>

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>${log4j.version}</version>
</dependency>

创建Web应用Initializer

public class BlogApplicationInitializer extends
        AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[0];
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{ WebConfig.class, ViewConfig.class, DBConfig.class, SecurityConfig.class};
    }

    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

getServletConfigClasses里的四个配置文件类分别用于配置Web组件、视图解析器相关组件、数据库相关组件和安全相关组件,目前我们的眼光会主要放在WebConfig.classDBConfig.class上,对于其它的配置文件,只需要创建出来并打上@Configuration注解即可。

初始化MyBatis

MyBatis对Spring项目提供了一个SqlSessionFactoryBean用于创建SqlSessionFactory,由于Spring是一个基于IoC的框架,MyBatis所生成的Mapper代理需要让Spring扫描到才能在项目中被随时注入,所以我们还需要创建一个MapperScannerConfigurer

首先创建数据源,db.properties要在你的classpath下并且提供了相应字段。

@Configuration
@PropertySource("classpath:db.properties")
public class DBConfig {

    @Bean
    public DataSource dataSource(
            @Value("${db.driver}") String driverClass,
            @Value("${db.url}") String url,
            @Value("${db.username}") String username,
            @Value("${db.password}") String password,
            @Value("${db.initialSize}") Integer initialSize,
            @Value("${db.maxActive}") Integer maxActive,
            @Value("${db.minIdle}") Integer minIdle,
            @Value("${db.maxWait}") Integer maxWait
    ) {
        System.out.println("CREATING druid datasource...");
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName(driverClass);
        druidDataSource.setUrl(url);
        druidDataSource.setUsername(username);
        druidDataSource.setPassword(password);
        druidDataSource.setInitialSize(initialSize);
        druidDataSource.setMaxActive(maxActive);
        druidDataSource.setMinIdle(minIdle);
        druidDataSource.setMaxWait(maxWait);
        return druidDataSource;
    }
}

坑1,username重复

这里配置文件中的属性名一定不要没有前缀,比如将db.username写成username,最好要有明确的前缀。我之前将它设置成username我发现解析的是我当前系统的用户名,而不是我在properties中指定的用户名。

因为这些东西都会注入到Environment对象中,系统环境变量也会注入到这里,然后取的时候可能会发生混淆。


其次创建sqlSessionFactory,作为全局的SqlSession工厂,这里设置了数据源,配置文件地址,以及mapper文件地址。

ClassPathResource很好理解,就是将文件名转换成对应的ClassPath下的Resource对象。而PathMatchingResourcePatternResolver是我之前没用过的,它的作用就是基于通配符来扫描生成Resource数组对象。

@Bean("sqlSessionFactory")
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws IOException {
    System.out.println("CREATING sqlSessionFactory...");
    SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(dataSource);
    sqlSessionFactoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
    sqlSessionFactoryBean.setMapperLocations(
            new PathMatchingResourcePatternResolver().getResources(
                    ResourceLoader.CLASSPATH_URL_PREFIX + "mappers/*.xml"
            )
    );
    return sqlSessionFactoryBean;
}

坑2,扫描到了这些文件作为Resource对象,可是Bean就是创建不起来

不知道啥原因,删除target输出文件夹,再进行编译就好了


配置MapperScannerbasePackage指定了扫描哪些包下的Mapper,其作用应该和在xml中配置差不多,但是这里我就全移动到Java中配置了。setSqlSessionFactoryBeanName用于设置刚刚创建的sqlSessionFactoryBean,然后注解类就用Spring的持久层注解类。

@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
    System.out.println("CREATING mapper scanner configurer...");
    MapperScannerConfigurer configurer = new MapperScannerConfigurer();
    configurer.setBasePackage("io.lilpig.blog.web.mapper");
    configurer.setSqlSessionFactoryBeanName("sqlSessionFactory");
    configurer.setAnnotationClass(Repository.class);
    return configurer;
}

创建配置文件

配置文件里,我们设置了一些基本的配置,并且将domain包下的所有实体类配置了自动扫描并生成别名

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="cacheEnabled" value="true"/>
        <setting name="useGeneratedKeys" value="true"/>
        <setting name="defaultExecutorType" value="REUSE"/>
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="defaultStatementTimeout" value="25000"/>
        <setting name="logImpl" value="LOG4J" />
    </settings>
    <typeAliases>
        <package name="io.lilpig.blog.web.domain"/>
    </typeAliases>
</configuration>

操作数据库

建库建表

drop database if exists blog;
create database blog;
use blog;
create table user(
    id int primary key auto_increment,
    username varchar(20) not null unique,
    password char(60) not null,
    email varchar(60) not null unique,
    gender tinyint(1) not null,
    message varchar(60),
    avater varchar(255),
    joinTime bigint not null,
    roles tinyint(1) not null,
    isEnabled tinyint(1) not null
);

有几个字段需要说明,gender类型代表性别,0为男,1为女,在Java中它的对应类型是枚举类,joinTime则是用户创建时的毫秒级unix时间戳,roles有点特别,它用一个tinyint来存储,应该是能存128(或者127),就是一个字节,有符号,也就是只有七位可用于存储。它的每一位代表用户是否具有一个角色,比如ADMINBLOGGER等,为1代表有,为0代表无,所以在我们的系统里最多也只能容纳七种角色。例如下面的插入示例这个字段为3,转换成二进制就是11,那么就代表它同时具有ADMINBLOGGER的权限。这里如果没看懂也没关系。

随便插入一条测试数据

insert into user(username, password, email, gender, message, avater, jointime, roles, isenabled)
    VALUES ('yulaoba','e10adc3950ba59abbe56e057f20f883e', '1355265122@qq.com', 1, null, null, 1633074332000, 3, 1);

创建domain对象

下面创建这个表对应的Java对象

public class BlogUser {
    private Long id;
    private String username;
    private String password;
    private String email;
    private Gender gender;
    private String message;
    private String avater;
    private Date joinTime;
    private List<String> roles;
    private Boolean isEnabled;

    // ... getter setter and toString ...

}

这里的gender是用枚举类型Gender来存储的,joinTime是用Date类型来存储的,roles是用List<String>类型来存储的(因为SpringSecurity需要String类型的角色信息),所以我们必须要考虑如何和数据库中对应列的不同数据类型进行转换,这时候就要用TypeHandler。

我先贴下Gender类

public enum Gender {

    MALE(0,"MALE"),
    FEMALE(1,"FEMALE");

    private final int id;
    private final String name;
    private Gender(int id, String name){
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return getName();
    }

    /**
     * 根据gender的id获取Gender对象
     * @param id 性别的id
     * @return 如果有与id匹配的性别,返回这个Gender对象,如果没有,则返回Gender.MALE
     */
    public static Gender getGender(int id) {
        for (Gender gender : values()) {
            if (gender.id == id) return gender;
        }
        return Gender.MALE;
    }
}

创建TypeHandler

Gender类的TypeHandler可以直接用org.apache.ibatis.type.EnumOrdinalTypeHandler,它会自动将枚举类型的ordinal(枚举实例在枚举类中所排的位置,从0开始)映射到数据库中的整数类型。这里在枚举类型中再存储一份和ordinal相同的id属性,是不想让外部访问语义不清晰的ordinal方法。

对于Date类型的joinTime转换成数据库中的bigint类型,则需要自行创建TypeHandler进行双向转换

@MappedJdbcTypes(JdbcType.BIGINT)
public class DateToUnixTimeStampTypeHandler extends BaseTypeHandler<Date> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Date parameter, JdbcType jdbcType) throws SQLException {
        ps.setLong(i, parameter.getTime());
    }

    @Override
    public Date getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return new Date(rs.getLong(columnName));
    }

    @Override
    public Date getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return new Date(rs.getLong(columnIndex));
    }

    @Override
    public Date getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return new Date(cs.getLong(columnIndex));
    }
}

对于List类型的roles转换成tinyint类型的数值,也需要自定义。这里把计算二者之间的转换的工作交给了RoleConvertUtils

@MappedJdbcTypes(JdbcType.TINYINT)
public class RoleTypeHandler extends BaseTypeHandler<List<String>> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException {
        ps.setLong(i,RoleConvertUtils.toIntFlag(parameter));
    }

    @Override
    public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return RoleConvertUtils.toRoleList(rs.getInt(columnName));
    }

    @Override
    public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return RoleConvertUtils.toRoleList(rs.getInt(columnIndex));
    }

    @Override
    public List<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return RoleConvertUtils.toRoleList(cs.getInt(columnIndex));
    }
}

这里是RoleConvertUtils

public class RoleConvertUtils {

    private final static Map<String, Integer> roleMap = new HashMap<>();
    static {
        roleMap.put("ADMIN", 1<<0);
        roleMap.put("BLOGGER", 1<<1);
    }

    public static List<String> toRoleList(int userRoles){
        List<String> roles = new ArrayList<>();

        for (Map.Entry<String, Integer> entry: roleMap.entrySet()) {
            if (hasRole(entry.getValue(), userRoles)) roles.add(entry.getKey());
        }
        return roles;
    }

    public static int toIntFlag(List<String> roles) {
        int flag = 0;
        for (String role: roles) {
            flag |= roleMap.get(role);
        }
        return flag;
    }

    private static boolean hasRole(int role, int userRoles) {
        return (role & userRoles) != 0;
    }
}

在mybatis配置文件中注册这些TypeHandler

<typeHandlers>
    <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"
                    javaType="io.lilpig.blog.constants.Gender"/>
    <typeHandler handler="io.lilpig.blog.db.typehandler.DateToUnixTimeStampTypeHandler"/>
    <typeHandler handler="io.lilpig.blog.db.typehandler.RoleTypeHandler"/>
</typeHandlers>

编写mapper

首先编写Mapper接口,这里面定义了四个查询方法,其它的方法后面会增加。对于登录和注册,我们需要的还有一个插入用户的方法

@Repository
public interface BlogUserMapper {
    List<BlogUser> getAllUsers();
    BlogUser getUserById(Long id);
    BlogUser getByUserName(String username);
    BlogUser getByEmail(String email);
}

然后编写mapper文件,我这里把所有的typeHandler应用都直接写成全限定名了,感觉用javaTypejdbcType会更加简洁。

<mapper namespace="io.lilpig.blog.web.mapper.BlogUserMapper">
    <resultMap id="blogUserMap" type="blogUser">
        <id property="id" column="id"/>
        <result property="username" column="username"/>
        <result property="password" column="password"/>
        <result property="email" column="email"/>
        <result property="gender" column="gender"
                typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
        <result property="message" column="message"/>
        <result property="avater" column="avater"/>
        <result property="joinTime" column="joinTime"
                typeHandler="io.lilpig.blog.db.typehandler.DateToUnixTimeStampTypeHandler"/>
        <result property="role" column="role"
                typeHandler="io.lilpig.blog.db.typehandler.RoleTypeHandler"/>
        <result property="isEnabled" column="isEnabled"/>
    </resultMap>

    <select id="getAllUsers" resultMap="blogUserMap">
        SELECT * FROM user
    </select>
    <select id="getUserById" parameterType="long" resultMap="blogUserMap">
        SELECT * FROM user WHERE id=#{id}
    </select>
    <select id="getByUserName" parameterType="string" resultMap="blogUserMap">
        SELECT * FROM user WHERE username=#{username}
    </select>
    <select id="getByEmail" parameterType="string" resultMap="blogUserMap">
        SELECT * FROM user WHERE email=#{email}
    </select>

</mapper>

视图解析器

视图解析器使用Thymeleaf。

<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring5</artifactId>
    <version>${thymeleaf.version}</version>
</dependency>

配置ViewConfig

@Configuration
public class ViewConfig {
    @Bean
    public SpringResourceTemplateResolver springResourceTemplateResolver() {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setPrefix("/WEB-INF/templates/");
        resolver.setSuffix(".html");
        resolver.setCacheable(false);
        resolver.setCharacterEncoding("UTF-8");
        resolver.setTemplateMode("HTML5");
        resolver.setOrder(1);
        return resolver;
    }

    @Bean
    public SpringTemplateEngine springTemplateEngine(SpringResourceTemplateResolver resolver) {
        SpringTemplateEngine engine = new SpringTemplateEngine();
        engine.setTemplateResolver(resolver);
        return engine;
    }

    @Bean
    public ViewResolver viewResolver(SpringTemplateEngine engine) {
        ThymeleafViewResolver resolver = new ThymeleafViewResolver();
        resolver.setTemplateEngine(engine);
        resolver.setCharacterEncoding("UTF-8");
        return resolver;
    }

}

编写控制器

@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    private BlogUserMapper blogUserMapper;


    @GetMapping("/{id}")
    public String getUserById(@PathVariable("id") Long id, Model model) {
        BlogUser blogUser = blogUserMapper.getUserById(id);
        model.addAttribute("user", blogUser);
        return "user";
    }
}

编写视图

<!-- user.html -->
<!doctype html>
<html lang="en">
<head>
    
    
    
    <title>Document</title>
</head>
<body>
    <h1 th:text="${user.username}"></h1>
</body>
</html>

现在打开浏览器并且使用为1的id访问上面的控制器,应该能够显示用户名了。

注册

校验

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>${hibernate.validator.version}</version>
</dependency>

添加校验注解

public class BlogUser {
    private Long id;
    @Size(min = 6, max = 20, message = "Username must in 6 ~ 20 characters.")
    private String username;
    @Size(min = 10, message = "Password must longer than 10 characters.")
    private String password;
    @Pattern(regexp = "[0-9a-zA-Z\\-_]+@[0-9a-zA-Z\\-_]+\\.[a-zA-Z0-9]+", message = "Incorrect email format")
    private String email;
    private Gender gender;
    private String message;
    private String avater;
    private Date joinTime;
    private List<String> roles;
    private Boolean isEnabled;

    // ... getter setter and toString ...

}

编写Mapper、Service、Controller

注册功能需要一个插入的SQL,在Mapper中添加对应方法

int create(BlogUser user);
<insert useGeneratedKeys="true" keyColumn="id" keyProperty="id" id="create" parameterType="blogUser">
    INSERT INTO user (username, password, gender, email, joinTime, roles, isEnabled)
        VALUES (#{username}, #{password},
                #{gender, typeHandler=org.apache.ibatis.type.EnumOrdinalTypeHandler},
                #{email}, #{joinTime, typeHandler=io.lilpig.blog.db.typehandler.DateToUnixTimeStampTypeHandler},
                #{roles, typeHandler=io.lilpig.blog.db.typehandler.RoleTypeHandler},
                #{isEnabled})
</insert>

因为在创建用户时有些字段不希望用户直接填写,比如joinTimerolesisEnabled和对密码进行BCrypt加密,所以这里使用一个Service层来处理这些功能,最后Service调用Mapper接口,进行插入。

创建Service层,这里引用到了一个passwordEncoder的Bean,现在还没有,等一会进行安全配置的时候会创建这个Bean。

@Service
public class BlogUserService {

    @Autowired
    private BlogUserMapper mapper;
    @Autowired
    private PasswordEncoder passwordEncoder;

    public BlogUser create(BlogUser user) {
        user.setPassword(
                passwordEncoder.encode(user.getPassword())
        );
        user.setRoles(List.of("BLOGGER"));
        user.setEnabled(true);
        user.setJoinTime(new Date());
        mapper.create(user);
        return user;
    }

}

编写Controller

@Controller
public class LoginRegisterController {

    @Autowired
    private BlogUserService blogUserService;

    @GetMapping("/register")
    public String register(Model model) {
        model.addAttribute("user", new BlogUser());
        return "register";
    }

    @PostMapping("/register")
    public String postRegister(
            @Valid @ModelAttribute("user") BlogUser user,
            BindingResult result
    ) {
        if (result.hasErrors()) {
            return "register";
        }
        blogUserService.create(user);
        return "redirect:/login";
    }

}

坑3,当出现error时,BindingResult和user不会自动注入到Model中

貌似,这个自动注入的操作是把类名当作model中的属性名的,所以我在页面上用user获取不到,而不是没有注入进去。加上@ModelAttribute指定名字即可。

个人觉得尽管代码多写点,但是也要把这些繁琐的东西都写上,尽量少依赖约定,最起码不会出错而且可读性会更好。


编写Converter

这里出现了一个问题,用户表单中传入的Gender是代表Gender id的字符串,而我们接收的对象中的gender属性是一个枚举类型,Spring无法自动完成转换。

这时候,需要编写一个Converter,在默认情况下返回男性,在能够匹配的情况下返回对应性别。

public class StringToGenderConverter implements Converter<String, Gender> {
    @Override
    public Gender convert(String source) {
        try {
            int genderFlg = Integer.parseInt(source);
            return Gender.getGender(genderFlg);
        }catch (Exception e) {
            return Gender.MALE;
        }
    }
}

然后我们需要在WebConfig中注册这个Converter

@EnableWebMvc
@Configuration
@ComponentScan("io.lilpig.blog.web")
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToGenderConverter());
    }
}

编写视图

<!doctype html>
<html lang="en">
<head>
    
    
    
    <title>Register | Blog</title>
</head>
<body>
    <h1>Register</h1>
    <p>1min to be a blogger!</p>
    <form id="form" th:action="@{/register}" method="post" th:object="${user}">

        <div class="errors" th:if="${#fields.hasAnyErrors()}">
            <ul>
                <li th:each="err : ${#fields.allErrors()}" th:text="${err}">Input is incorrect</li>
            </ul>
        </div>

        EMAIL: <input type="text" name="email" th:field="${user.email}"> <br>
        USERNAME: <input type="text" name="username" th:field="${user.username}"> <br>
        PASSWORD: <input type="password" name="password" id="password"> <br>
        CONFIRM PASSWORD: <input type="password" id="confirm"> <br>
        GENDER:
        <label><input type="radio" checked name="gender" value=1> MALE </label>
        <label><input type="radio" name="gender" value=2> FEMALE </label> <br>
        <button type="button" onclick="register()">Register</button>
    </form>
    <script>
        function register() {
            var confirm = document.getElementById("confirm");
            var password = document.getElementById("password");
            if (confirm.value == password.value) {
                var form = document.getElementById("form");
                form.submit();
            }else {
                alert("两次密码不匹配");
            }
        }
    </script>
</body>
</html>

安全

导入依赖

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-bom</artifactId>
            <version>${spring.security.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
</dependency>

配置

编写一个Initialzier用于初始化Spring的安全FilterChain

public class WebSecurityInitializer extends AbstractSecurityWebApplicationInitializer {

}

下面就是完善我们之前的SecurityConfig,这里,使用了BcryptPasswordEncoder作为密码编码器,它是一个使用随机盐值+慢散列函数对原始密码进行加密的编码器,由于有随机盐值,拖库和爆破都很难拿到用户的原始密码,慢散列让暴力破解的时间更长,几乎无法爆破。BlogUserDetailsService则是我们配置的用于Spring进行安全认证的一个服务类,用于和数据库中的数据进行交互,DaoAuthenticationProvider将上面两个Bean,也就是密码编码器和用户认证服务整合了,没啥其它特殊工作,其实不整合也行。

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    public BlogUserDetailsService userDetailsService() {
        return new BlogUserDetailsService();
    }

    @Bean
    public DaoAuthenticationProvider authProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(blogUserDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder);
        return authProvider;
    }

    @Autowired
    private BlogUserDetailsService blogUserDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private DaoAuthenticationProvider authenticationProvider;


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginProcessingUrl("/authentication/login").loginPage("/login")
                .and().authorizeRequests()
                .antMatchers("/control").hasAuthority("ROLE_BLOGGER")
                .anyRequest().permitAll();
    }
}

在第一个configure中,我们把密码编码器和用户认证服务的组合传了进去,告诉Spring如何进行认证,第二个configure配置了登录页面以及登录要post的地址,定义了在何地进行认证,后面又匹配了一些url,对control这个链接进行认证,要求其必须有BLOGGER角色,其它的链接全部允许,这定义了何时进行认证。

编写BlogUserDetailsService

这个类是提供给Spring的用户认证服务类,其主要作用就是通过Mapper,根据用户名查询用户,返回给Spring。Spring会校验用户的密码和登录密码是否一致。

public class BlogUserDetailsService implements UserDetailsService {
    @Autowired
    private BlogUserMapper blogUserMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        BlogUser blogUser = blogUserMapper.getByUserName(s);
        if (blogUser==null) throw new UsernameNotFoundException(s);
        return new BlogUserDetails(blogUser);
    }
}

编写BlogUserDetails

这里之所以又包装了一层,是为了可以将BlogUser和Spring的认证绑定,要么后面只能通过认证对象访问到用户名什么的,像BlogUser中特有的信息(主要是id,其他可以改的字段我们不能依赖这个对象,可能会读到过时数据),我们只能再查询数据库获取。

public class BlogUserDetails implements UserDetails {

    private final BlogUser blogUser;

    public BlogUserDetails(BlogUser blogUser) {
        this.blogUser = blogUser;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        this.blogUser.getRoles().forEach(roleName->{
            authorities.add(new SimpleGrantedAuthority("ROLE_"+roleName));
        });
        return authorities;
    }

    @Override
    public String getPassword() {
        return blogUser.getPassword();
    }

    @Override
    public String getUsername() {
        return blogUser.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return blogUser.getEnabled();
    }

    public BlogUser getBlogUser() {
        return this.blogUser;
    }
}

编写Controller方法

@GetMapping("/login")
public String login(Model model) {
    model.addAttribute("user", new BlogUser());
    return "login";
}

编写页面

表单的地址要是定义的登录处理地址,这样Spring才能进行登录校验

<!DOCTYPE html>
<html lang="en">
<head>
    
    <title>Login | Blog</title>
</head>
<body>
    <h1>Login To BLOG</h1>
    <p>After login, you can create your blog for free, and post blog no limit!</p>
    <form th:action="@{/authentication/login}" method="post" th:object="${user}">
        USERNAME: <input type="text" name="username" th:field="${user.username}"> <br>
        PASSWORD: <input type="text" name="password"> <br>
        <button type="submit">Login</button>
        <br>
        <a href="#">I forgot my password!</a> | <a th:href="@{/register}">Register</a>
    </form>
</body>
</html>
posted @ 2021-10-03 10:46  yudoge  阅读(70)  评论(0编辑  收藏  举报