这里是登录注册后端部分代码及思路,前端请访问:
Spring boot + Mybatis 从零开始搭建个人博客系统(三)——登录注册(前端)
数据表建立
p.s. 很多人喜欢先建表再设计页面设计功能,但这一点很可能导致你后期为了功能而回来修改表,添加字段或删减字段,可能会给自己造成很大的麻烦,所以我建议这里先设计个人中心的页面,思考什么功能会用到用户表,我的用户表应该设计什么字段,这些字段应该含有什么属性,后期可能会少很多不必要的麻烦。
用户表
名称 | 类型 | 非空 | 默认 | 主键 | 描述 |
---|---|---|---|---|---|
id | varchar | ture | 无 | true | 主键用户ID标识 |
gender | tinyint | ture | 无 | false | 性别 |
user_name | varchar | ture | 无 | false | 昵称 |
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插件项目地址,请自行安装。
用户
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。
“登录”流程
确定流程
我们首先需要确定登录的流程,这样我们才能根据各个功能进行针对性的配置。
- 当用户访问登录页面时,我们需要保存用户请求前的URL,方便登录后跳转回用户访问前的页面。
- 当用户登录时,我们需要为用户赋予USER权限,并比对密码。
- 当用户登录成功时,我们需要将用户的“最近登录时间”更新。
请求前页面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个步骤:
- 用户用
username
和password
进行登录。
其中,username
和password
会被封装为一个Authentication
接口实例。 - 验证密码正确性
这里会将第一步封装好的Authentication
传递给AuthentiacationManager
进行验证,它的实现类会调用UserDetailsService
对用户信息进行封装并返回一个UserDetails
,之后对这个UserDetails
进行信息检查并用PasswordEncoder
进行密码验证。 - 建立安全用户上下文。
当认证成功后会返回一个经过认证后的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。
(如果非拦截,则savedRequest
为null,将委托给上层处理,最后跳转到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
中包含用户的上下文信息,这里getName
为UserDetails
保存的用户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
加密,这里是将数据库取出的加密密码(也就是自定义UserDetails
中user.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
可以被拥有USER
和ADMIN
权限的用户访问。
当用户未登录时,访问该路径会被拦截到登录页面进行登录认证。
@AuthenticationPrincipal
用来获取登录后的用户的上下文信息,principal.getName()
就是authentication
中保存的用户信息,也就是自定义UserDetails
中最后加入的用户ID。
页面的权限认证以及登录认证我用到了Themeleaf
模板,这部分在我的前端文章中可以看到,这里就不做过多的讲解了。