Spring boot + Mybatis 从零开始搭建个人博客系统(四)——登录注册(后端)

这里是登录注册后端部分代码及思路,前端请访问:
Spring boot + Mybatis 从零开始搭建个人博客系统(三)——登录注册(前端)

数据表建立

p.s. 很多人喜欢先建表再设计页面设计功能,但这一点很可能导致你后期为了功能而回来修改表,添加字段或删减字段,可能会给自己造成很大的麻烦,所以我建议这里先设计个人中心的页面,思考什么功能会用到用户表,我的用户表应该设计什么字段,这些字段应该含有什么属性,后期可能会少很多不必要的麻烦。

用户表

名称 类型 非空 默认 主键 描述
id varchar ture true 主键用户ID标识
gender tinyint ture false 性别
user_name varchar ture false 昵称
email varchar false false 电子邮箱地址
birthday varchar false false 生日
image_url varchar false false 头像外链地址
recent_login_date timestamp false false 用户最近登录时间
phone varchar true false 手机号
password varchar true false 密码
name varchar false false 真实姓名
introduce varchar false false 个人介绍

除了用户表之外,我们还需要给用户赋予权限,所以需要一个权限表与用户权限对应表。
(网上有一些教程只建了用户和用户权限关系两张表,这是很不可取的,具体原因可以参考另一篇文章——数据库三大范式理解与Mybatis懒加载

权限表

名称 类型 非空 默认 主键 描述
id int ture true 权限ID
name varchar ture false 权限名称

用户权限关系表

名称 类型 非空 默认 主键 描述
user_id varchar ture false 用户ID
role_id int ture false 权限ID

实体类

实体类的建立我用了 Lombok 来简化代码,首先引入 Lombok 的包。

<!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
</dependency>

具体用法请查看 Lombok文档

其次我们还要安装 Lombok 的插件:
Lombok
附上 Lombok插件项目地址,请自行安装。

用户

package com.seagull.myblog.model;

import lombok.*;

import java.util.Date;

/**
 * @author Seagull_gby
 * @date 2019/3/21 20:43
 * Description: 用户类
 */

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

    /**
     * 独特标识ID
     */
    private String id;

    /**
     * 性别(男1女2)
     */
    private int gender;

    /**
     * 昵称
     */
    private String userName;

    /**
     * 邮箱
     */
    private String email;

    /**
     * 生日
     */
    private String birthday;

    /**
     * 手机号
     */
    private String phone;

    /**
     * 头像URL
     */
    private String imageUrl;

    /**
     * 权限
     */
    private Role role;

    /**
     * 最近登录日期
     */
    private Date recentLoginDate;

    /**
     * 密码
     */
    private String password;

    /**
     * 真实姓名
     */
    private String name;

    /**
     * 个人介绍
     */
    private String introduce;
}

权限实体

package com.seagull.myblog.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author Seagull_gby
 * @date 2019/3/21 20:59
 * Description: 权限实体类
 */

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Role {

    /**
     * ID
     */
    private int id;

    /**
     * 权限名字
     */
    private String name;
}

“注册”流程

注册中验证码的控制我用到了redis进行缓存,这里先讲一下redis如何配置。

redis 配置

首先加入maven:

<!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-redis</artifactId>
            <version>1.3.8.RELEASE</version>
        </dependency>

之后对redis进行配置:

# redis配置
spring.redis.host= 127.0.0.1
spring.redis.port= 6379
spring.redis.password=****
spring.redis.pool.max-active= 100
spring.redis.pool.max-idle= 10
spring.redis.pool.max-wait= 100000
spring.redis.timeout= 0

关于redis的配置方法有很多,网上也有很多的介绍,我使用的是Jackson2JsonRedisSerialize进行序列化配置:

/**
 * @author Seagull_gby
 * @date 2019/3/28 16:51
 * Description: redis 配置类
 */
@Configuration
@EnableAutoConfiguration
public class RedisConfig {
    
    /**
     * redisTemplate 序列化使用的jdkSerializeable, 存储二进制字节码, 所以自定义序列化类
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 使用Jackson2JsonRedisSerialize 替换默认序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 设置value的序列化规则和 key的序列化规则
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

编写redis的接口与实现类,这里可以根据自己的需要进行功能的增添和删减,具体方法可以去查阅RedisTemplate的用法
接口:

/**
 * @author Seagull_gby
 * @date 2019/3/28 16:53
 * Description: Redis 接口
 */
public interface RedisService {

    /**
     * set存数据
     * @param key
     * @param value
     */
    public void set(Object key, Object value);

    /**
     * 存数据并设置过期时间(秒级)
     * @param key 键
     * @param value 值
     * @param timeOut 过期时间(秒)
     */
    public void setAndTimeOut(Object key, Object value, long timeOut);

    /**
     * 设置键的字符串并返回旧值
     * @param key 键
     * @param value 新值
     * @return 旧值
     */
    public Object getAndSet(Object key, Object value);

    /**
     * get获取数据
     * @param key
     */
    public Object get(Object key);

    /**
     * 设置有效秒数
     * @param key
     * @param expire 秒数
     */
    public void expire(Object key, long expire);

    /**
     * 移除数据
     * @param key
     */
    public void remove(Object key);

    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false 不存在
     */
    public boolean hasKey(Object key);

    /**
     * 添加一个值到对应键的set集合中
     * @param key 键
     * @param value 值
     */
    public void sadd(Object key, Object value);

    /**
     * 获得某个键中的值的集合
     * @param key 键
     * @return value集合
     */
    public Set members(Object key);

    /**
     * 添加map到hset中
     * @param key 键
     * @param hashKey map的键
     * @param value map的值
     */
    public void hset(Object key, Object hashKey, Object value);

    /**
     * 根据key和map的key取值
     * @param key 键
     * @param hashKey map的键
     * @return 对应的值
     */
    public Object hget(Object key, Object hashKey);

    /**
     * 删除对应key和map的key的值
     * @param key 键
     * @param hashKey map的键
     */
    public void deleteHsetValue(Object key, Object hashKey);
}

实现:

/**
 * @author Seagull_gby
 * @date 2019/3/28 16:53
 * Description: Redis 实现
 */
@Service("redisService")
public class RedisServiceImpl implements RedisService {

    @Resource
    private RedisTemplate<Object, Object> redisTemplate;

    @Override
    public void set(Object key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    @Override
    public void setAndTimeOut(Object key, Object value, long timeOut) {
        redisTemplate.opsForValue().set(key, value, timeOut, TimeUnit.SECONDS);
    }

    @Override
    public Object getAndSet(Object key, Object value) {
        return redisTemplate.opsForValue().getAndSet(key, value);
    }

    @Override
    public Object get(Object key) {
        return redisTemplate.opsForValue().get(key);
    }

    @Override
    public void expire(Object key, long expire) {
        redisTemplate.expire(key, expire, TimeUnit.SECONDS);
    }

    @Override
    public void remove(Object key) {
        redisTemplate.delete(key);
    }

    @Override
    public boolean hasKey(Object key) {
        return redisTemplate.hasKey(key);
    }

    @Override
    public void sadd(Object key, Object value) {
        redisTemplate.opsForSet().add(key, value);
    }

    @Override
    public Set members(Object key) {
        return redisTemplate.opsForSet().members(key);
    }

    @Override
    public void hset(Object key, Object hashKey, Object value) {
        redisTemplate.opsForHash().put(key, hashKey, value);
    }

    @Override
    public Object hget(Object key, Object hashKey) {
        return redisTemplate.opsForHash().get(key, hashKey);
    }

    @Override
    public void deleteHsetValue(Object key, Object hashKey) {
        redisTemplate.opsForHash().delete(key, hashKey);
    }
}

验证码发送

验证码发送我用到了阿里云的“短信发送”功能,使用需要去阿里云官网申请模板和信息。
阿里云短信发送代码:

/**
     * 阿里云短信单发服务
     * @param phone 电话号码
     * @param code 验证码
     * @param type 模板选择(1为注册,2为修改密码,3为找回密码)
     * @return 目标JSON
     * @throws ClientException
     */
    public static SendSmsResponse sendSms(String phone, int code, int type) throws ClientException {
        /* 设置超时时间 */
        System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
        System.setProperty("sun.net.client.defaultReadTimeout", "10000");

        /* 初始化ascClient需要的几个参数 */
        final String product = "Dysmsapi";
        final String domain = "dysmsapi.aliyuncs.com";

        final String accessKeyId = ACCESS_KEY_ID;
        final String accessKeySecret = ACCESS_KEY_SECRET;

        /* 初始化ascClient,暂时不支持多region */
        IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId,
                accessKeySecret);
        DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
        IAcsClient acsClient = new DefaultAcsClient(profile);

        /* 组装请求对象 */
        SendSmsRequest request = new SendSmsRequest();
        request.setMethod(MethodType.POST);
        request.setPhoneNumbers(phone);
        request.setSignName("Seaguller");
        if(type == 1) {
            request.setTemplateCode(REGISTER_PHONE_TEMPLATE);
        } else if(type == 2) {
            request.setTemplateCode(SAFETY_PHONE_TEMPLATE);
        } else {
            request.setTemplateCode(RETRIEVE_PHONE_TEMPLATE);
        }
        JSONObject smsJSON = new JSONObject();
        smsJSON.put("code", code);
        request.setTemplateParam(String.valueOf(smsJSON));

        SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
        if(sendSmsResponse.getCode() != null && sendSmsResponse.getCode().equals("OK")) {
            System.out.println("短信发送成功!");
        } else {
            System.out.println("短信发送失败!");
        }

        return sendSmsResponse;
    }

因为我的项目有三个地方用到了验证码发送,所以加入了三套不同的模板,用type进行控制。
(ACCESS密钥等阿里云认证后可以用RAM子用户赋予短信服务的权限进行配置,提高安全性)

具体代码实现:

    public JSONObject sendPhoneCode(String phone, int type) throws ClientException {
        JSONObject spc = new JSONObject();

        int sixRandow = randomNum.getSixRandomNum();
        redisService.setAndTimeOut(phone, sixRandow, 180);

        SendSmsResponse sendSmsResponse = AliyunClientUtil.sendSms(phone, sixRandow, type);

        if(sendSmsResponse.getCode() != null && sendSmsResponse.getCode().equals("OK")) {
            spc.put("code", 200);
            spc.put("msg", "success");
        } else {
            spc.put("code", sendSmsResponse.getCode());
            spc.put("msg", sendSmsResponse.getMessage());
        }

        return spc;
    }

randomNum.getSixRandomNum() 是获取一个随机六位数作为验证码。
获取随机数代码如下:

/**
     * 获取6位随机数
     * @return
     */
    public int getSixRandomNum() {
        int randomNum;

        randomNum = (int) ((Math.random()*9+1) * 100000);

        return randomNum;
    }

redisService.setAndTimeOut(phone, sixRandow, 180); 是将手机与验证码作为键值对缓存到redis中,过期时间设置为180秒。
(验证验证码正确性时直接调用redisService.hasKey(phone)看键值对存不存在,存在则取出对应的值进行验证即可)

数据库添加用户

/**
     * 注册用户
     * @param request 请求域
     * @return 页面
     */
    @RequestMapping("/registerUser")
    public String registerUser(HttpServletRequest request) {
        User user = new User();

        user.setUserName(request.getParameter("userName"));
        user.setPassword(request.getParameter("password"));
        user.setPhone(request.getParameter("phone"));

        String sex = request.getParameter("gender");
        if(sex.equals("male")) {
            user.setGender(1);
            user.setImageUrl(DEFAULT_BOY_IMG);
        } else {
            user.setGender(0);
            user.setImageUrl(DEFAULT_GIRL_IMG);
        }

        registerService.insertUser(user);

        return "registerSuccess";
    }

这里只有一点,就是根据用户的性别添加默认的用户头像。
这里我用到了阿里云的OSS存储,在阿里云官网控制台开放OSS功能后,将存储空间设置为公共读(若保持私密性请勿进行设置而采用其他操作),上传图片即可直接调用图片的URL外链访问图片,这里将我上传的用户默认头像外链根据性别分别添加。

private static final int ROLE_USER = 2;

public void insertUser(User user) {
        /* 用UUID作为用户唯一ID存储 */
        String userId = UUID.randomUUID().toString().replace("-", "");
        user.setId(userId);

        /* 密码加密 */
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        user.setPassword(encoder.encode(user.getPassword()));

        userMapper.insertUser(user);
        roleMapper.insertUserRole(userId, ROLE_USER);

    }

因为我后续准备加入QQ注册登录的功能,所以没有在数据库层面实现UUID的创建,而是用java自带的工具包创建UUID作为用户的唯一ID。

BCryptPasswordEncoder进行密码加密,解密请看下面登录过程的security配置。

注意,当添加用户时要同时添加用户的权限信息,新用户默认权限为:USER。

“登录”流程

确定流程

我们首先需要确定登录的流程,这样我们才能根据各个功能进行针对性的配置。

  1. 当用户访问登录页面时,我们需要保存用户请求前的URL,方便登录后跳转回用户访问前的页面。
  2. 当用户登录时,我们需要为用户赋予USER权限,并比对密码。
  3. 当用户登录成功时,我们需要将用户的“最近登录时间”更新。

请求前页面URL保存

这个我用到了请求头中的referrer报文头,当发出请求时一般会带上这个报文头,告诉服务器用户从哪个页面链接过来的,也就是说里面储存了用户跳转到登录页面时所在页面的URL。

我们可以在用户访问登录页面时(也就是访问/login时)将 referrer 携带的URL保存在 session 中:

@GetMapping("/login")
public String loginJump(HttpServletRequest request) {

        request.getSession().setAttribute("url", request.getHeader("Referer"));

        return "login";
}

这样我们只需要在登录检查成功后从session中取出url并跳转即可。

Spring security 配置

关于权限管理我使用的是 Spring security ,这里我会就各个功能详细讲一下我的 Spring security配置。

简单流程

在配置之前首先需要简单熟悉一下 Spring security 的工作流程。
这里我简单分为了3个步骤:

  1. 用户用usernamepassword进行登录。
    其中,usernamepassword会被封装为一个Authentication接口实例。
  2. 验证密码正确性
    这里会将第一步封装好的Authentication传递给AuthentiacationManager进行验证,它的实现类会调用UserDetailsService对用户信息进行封装并返回一个UserDetails,之后对这个UserDetails进行信息检查并用PasswordEncoder进行密码验证。
  3. 建立安全用户上下文。
    当认证成功后会返回一个经过认证后的Authentication,里面包含了用户的信息,并调用successHandler进行成功后的处理操作。

这里我只是简单介绍了下流程,了解之后我们就知道该在什么地方重写什么方法来达成我们的功能。
如果对具体的认证流程感兴趣,可以去查一下 Spring security 认证流程详解,这里不做过多介绍。

用户权限认证

看流程的第2步,对用户信息封装用的是UserDetailsService接口,而我们赋予用户权限信息,恰恰就是在对用户信息封装的过程中。
所以实现UserDetailsService接口,重写loadUserByUsername方法:

/**
 * @author Seagull_gby
 * @date 2019/3/23 12:51
 * Description: 权限认证将用户名与权限关系储存
 */
@Service
public class CustomUserService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) {

        User user = userMapper.queryUserByPhone(username);

        if(user == null){
            throw new UsernameNotFoundException("用户名不存在");
        }
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();

        authorities.add(new SimpleGrantedAuthority(user.getRole().getName()));

        return new org.springframework.security.core.userdetails.User(user.getId(),
                user.getPassword(), authorities);
    }
}

这里传入的username为前端表单提交的username,我这里提交的是用户的手机号,所以用手机号将用户取出,并添加用户权限,将用户ID(非用户名)、密码和权限信息包装返回。
(建立用户上下文,这里用户ID也可以选择用用户名,这里设置的什么,后面用authentication获取用户信息时得到的就是什么)

用户的权限信息我使用的是关联查询,本应用@Many,但因为项目需求不需要多对多关系,一对一就足够,所以我用的@One,用户实体类中也是Role而不是List<Role>:

/**
     * 查询用户(连带权限信息)
     * @param phone 用户手机号(用户名)
     * @return 单个用户实体
     */
    @Results(
            id = "user", value = {
            @Result(property = "role", column = "id", one = @One(fetchType= FetchType.LAZY, select = "com.seagull.myblog.mapper.RoleMapper.queryUserRole")),
            @Result(property = "id", column = "id")
    }
    )
    @Select("SELECT * FROM user WHERE phone LIKE #{phone}")
    public User queryUserByPhone(String phone);

@One表示一对一查询,将用户的权限信息查询出来。
fetchType= FetchType.LAZY表示开启懒加载,只有用到权限信息时才会执行该关联查询,否则仅执行单条查询语句而不查询权限。
权限查询:

/**
     * 用嵌套查询某用户权限
     * @param userId 用户ID
     * @return 权限信息
     */
    @Select("SELECT * FROM role WHERE id = (SELECT role_id FROM user_role WHERE user_id LIKE #{userId})")
    public Role queryUserRole(String userId);

自定义登录后跳转及更新登录时间

从流程中我们知道,当登录成功后会将用户信息交给successHandler进行处理。
路径重定向具体的实现类,我们可以通过继承SavedRequestAwareAuthenticationSuccessHandler类(AuthenticationSuccessHandler接口的实现类)并重写onAuthenticationSuccess方法来进行处理。

这里多说一句,我们为什么不直接实现AuthenticationSuccessHandler接口而去继承SavedRequestAwareAuthenticationSuccessHandler这个类?
我们来看SavedRequestAwareAuthenticationSuccessHandler中重写的onAuthenticationSuccess 方法源码:

@Override
	public void onAuthenticationSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication authentication)
			throws ServletException, IOException {
		SavedRequest savedRequest = requestCache.getRequest(request, response);

		if (savedRequest == null) {
			super.onAuthenticationSuccess(request, response, authentication);

			return;
		}
		String targetUrlParameter = getTargetUrlParameter();
		if (isAlwaysUseDefaultTargetUrl()
				|| (targetUrlParameter != null && StringUtils.hasText(request
						.getParameter(targetUrlParameter)))) {
			requestCache.removeRequest(request, response);
			super.onAuthenticationSuccess(request, response, authentication);

			return;
		}

		clearAuthenticationAttributes(request);

		// Use the DefaultSavedRequest URL
		String targetUrl = savedRequest.getRedirectUrl();
		logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
		getRedirectStrategy().sendRedirect(request, response, targetUrl);
	}

从中可以看出,SavedRequestAwareAuthenticationSuccessHandler类实现了对上次请求页面URL的保存。
比如我要请求 admin.html 这个页面,被拦截到了登录页,那么当我登录成功后,会自动跳转到admin.html这个页面

既然它已经自己实现了登录成功后的跳转,为什么我们还要自己实现?还要保存referrer
请注意,我一直在强调 页面 两个字,也就是说,它的保存是针对对页面跳转操作拦截的保存。
比如,我在文章页面要发表评论,被拦截到了登录页,这种访问URL被拦截的情况登录后是不会跳回文章页的。
再比如,我在“归档”页面,自己跳转到了登录页,这种自己跳转的方式,它同样是不会来储存的。

它只会储存针对页面跳转拦截后的URL,就像我直接去访问/admin,本应跳转到admin.html这个页面,但是因为这个页面要验证权限,所以会被拦截到登录页,这时才会去对/admin这个URL进行储存。
而如果不是针对页面的拦截,则成功后会跳转到用户第一次访问网站时的URL

这个其实从源码层次上看会更加清晰。当访问受保护的资源时,ExceptionTranslationFilter会捕获到AuthenticationException异常,在跳转前使用RequestCache缓存request
而缓存后的request又在onAuthenticationSuccess中被取出,用getRedirectUrl()获取URL。
(如果非拦截,则savedRequestnull,将委托给上层处理,最后跳转到defaultTargetUrl,也就是/(首页))
我这里不做过多讲解,感兴趣的可以去看RequestCache的实现类HttpSessionRequestCache的源码和AbstractAuthenticationTargetUrlRequestHandler的源码。

自定义successHandler

/**
 * @author Seagull_gby
 * @date 2019/3/23 13:44
 * Description: 登录成功后自定义跳转路径(原始路径)
 */

@Component("myAuthenticationSuccessHandler")
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Autowired
    private UserMapper userMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {

        /* 登录成功更新登录时间 */
        userMapper.updateRecentLoginDate(authentication.getName(), new Date());

        if(request.getSession().getAttribute("url")!=null){
            //如果是要跳转到某个页面的
            new DefaultRedirectStrategy().sendRedirect(request, response,(String)request.getSession().getAttribute("url"));
            request.getSession().removeAttribute("url");
        } else {
            super.onAuthenticationSuccess(request, response, authentication);
        }

    }
}

如果session中有保存url,则获取到后对页面进行重定向,否则直接委托给上层处理。
因为这里已经认证成功,所以我们在这里更新了用户的最近登录时间。
authentication中包含用户的上下文信息,这里getNameUserDetails保存的用户ID)

Spring security 具体配置代码

/**
 * @author Seagull_gby
 * @date 2019/3/5 20:10
 * Description: 安全框架
 */

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .authorizeRequests()
                // 所有用户均可访问的资源
                .antMatchers("/css/**", "/js/**", "/images/**", "/webjars/**", "**/favicon.ico").permitAll()
                .anyRequest().permitAll()
                .and()
                .formLogin()
                // 指定登录页面,授予所有用户访问登录页面
                .loginPage("/login")
                .successHandler(myAuthenticationSuccessHandler)
                .loginProcessingUrl("/loginCheck")
                .failureUrl("/login?error").permitAll()
                .and()
                //开启cookie保存用户数据
                .rememberMe()
                //设置cookie有效期
                .tokenValiditySeconds(60 * 60 * 24 * 7)
                //设置cookie的私钥
                .key("security")
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/index")
                .permitAll()
                .and()
                .csrf().disable();

    }

    @Bean
    UserDetailsService customUserService() {
        return new CustomUserService();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserService()).passwordEncoder(new BCryptPasswordEncoder());
    }

}

.antMatchers("/css/**", "/js/**", "/images/**", "/webjars/**", "**/favicon.ico").permitAll() :表示允许静态资源访问。

.anyRequest().permitAll() :表示开放所有路径请求。
(因为我权限控制用的是 @PreAuthorize("hasAnyRole('ADMIN', 'USER')")注解加在了接口上,所以这里不需要再加入额外的权限路径拦截,而且这里加权限路径拦截会有一个小BUG,当你进行Ajax请求时,如果在这里加拦截的话的确会拦截到,但是被拦截到后只是发送了一次重定向的请求,由于Ajax的缘故将不会对登录页进行跳转处理)

.loginPage("/login") :指定登录URL为/login

.successHandler(myAuthenticationSuccessHandler):自定义认证成功后处理操作。

.loginProcessingUrl("/loginCheck"):这个是将认证操作全权委托给了Security,不需要针对/loginCheck写额外的接口,前端提交表单提交到/loginCheck即可,会自动执行认证。

.failureUrl("/login?error"):登录失败(可能是密码错误也可能是用户名错误)后跳转URL,这里在后面加了个error字段,在页面用Themeleaf模板的th:if="${param.error}可以对登录失败进行检查。

configure方法中的customUserService()).passwordEncoder(new BCryptPasswordEncoder():我在注册时对用户密码进行了BCrypt加密,这里是将数据库取出的加密密码(也就是自定义UserDetailsuser.getPassword()放入的密码)解密。

权限拦截

权限拦截主要用到了 @PreAuthorize("hasAnyRole('ADMIN', 'USER')") 注解,hasAnyRole里面标明该接口允许被哪些权限的用户访问(不用加“ROLE_”前缀)
举例:

@PreAuthorize("hasAnyRole('ADMIN', 'USER')")
    @ResponseBody
    @RequestMapping("/getUserInformation")
    public JSONObject getUserInformation(@AuthenticationPrincipal Principal principal) {
        JSONObject userInformation = new JSONObject();
        userInformation.put("code", 200);

        if(principal==null) {
            userInformation.put("msg", "noLogin");
        } else {
            userService.getUserInformation(userInformation, principal.getName());
            userInformation.put("msg", "success");
        }

        return userInformation;
    }

@PreAuthorize("hasAnyRole('ADMIN', 'USER')") 标明 /getUserInformation 可以被拥有USERADMIN权限的用户访问。
当用户未登录时,访问该路径会被拦截到登录页面进行登录认证。

@AuthenticationPrincipal 用来获取登录后的用户的上下文信息,principal.getName()就是authentication中保存的用户信息,也就是自定义UserDetails中最后加入的用户ID。

页面的权限认证以及登录认证我用到了Themeleaf模板,这部分在我的前端文章中可以看到,这里就不做过多的讲解了。

猜你喜欢

转载自blog.csdn.net/abc67509227/article/details/89524990