我好菜啊,学了好几天才明白一点点
把v部落git下来学一学,比起halo来说v部落会更加简单好懂一点。我看他用了SpringSecurity来做登录验证,那第一步就是学学这个SpringSecurity。
然后我就发现了,我真的是太菜了,看博客,看视频都不尽如意。尤其是用vue配合SpringSecurity的情况下实在是费劲,看了好多资料感觉都不是我需要的,懂得人感觉特别简单,不懂的就很费劲,说来说去还是我太菜了。
我也是看了好几天资料并结合v部落的代码,才算是琢磨出来一点点门道,所以就记录一下我这个学习的成果。主要是vue配合SpringSecurity来使用,双方互相用json传递数据。学习之前需要懂得以下技术:
首先我们需要有以下两三个页面:
这几个页面我是用vue写的,大家有时间也可以自己写写,当然部分代码我也是参考别人的,虽然有那么一点点缺陷,但不影响使用。我把页面放在我的码云上面,不想写的话可以git下来。
码云地址:https://gitee.com/siumu/blog_code.git
界面长这个样子:
接下来就是准备后端的代码了,先创建一个项目,再建立一个数据库。刚开始自然是创建项目,在pom文件里把Spring Boot,SpringSecurity,MyBatis等等一些东西,以下就是我的pom依赖:
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 使用undertow替换tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- hutool开源JSON工具 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
<version>5.2.4</version>
</dependency>
</dependencies>
这里面有的是自带的,有的是我后来加上去的。也没啥复杂的东西。
既然是用户角色权限控制,那自然就需要有一个用户表,一个角色表。根据v部落设计的数据库,一个用户可以有多个角色,所以用户与角色之间就是多对多的关系,那么就需要一个用户角色关系表。
总结一下就是需要三张表分别是用户表、角色表、用户角色关系表。
数据库我也放在码云上,直接导入sql文件即可
接下来自然是建立实体类,跟数据库一一对应,这里也不复杂介绍,反正用的是v部落的数据库,看看字段注释就知道啥意思了。
首先是用户实体类
@Data
public class User implements UserDetails {
private Long id; //主键
private String username; //用户名
private String password; //密码
private String nickname; //昵称
private boolean enabled; //是否禁用
private List<Role> roles; //用户角色
private String email; //邮箱
private String userface; //头像
private Timestamp regTime; //注册时间
@Override
public List<GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
}
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
}
为什么要实现UserDetails
这个接口呢?如果了解过这方面的知识就会知道,使用SpringSecurity从数据库里拿用户信息需要实现这个接口,这个接口,提供了一系列的方法,比如账户是否过期啊,用户是否被锁定啊等等。
其中有些字段我们数据库里有,比如用户名,密码,是否禁用之类的。但是有些就没有,所以我们需要重写这些方法,然后让他们统统返回true
。如果不了解可以百度一下这方面的博客看一看,我们现在直接用它就行了。
然后是角色实体类
@Data
public class Role {
/**
* 主键
*/
private Long id;
/**
* 角色名称
*/
private String name;
}
实体类就这么简单的完成了。
接下来就是配置数据库连接了,这应该简单,我就直接把application.yml放上来吧。
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/dbgirl?characterEncoding=utf8&useSSL=false
username: root
password: 123456
server:
port: 8080
接下来就是写操作数据库部分了,大家熟悉什么就用什么,我是看MyBatis比较火,所以我也用MyBatis。
以下是mapper层的接口与xml:
public interface UserMapper {
/**
* 根据用户名查询用户信息
* @param username 用户名
* @return
*/
User selectByUserName(String username);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiumu.securitydemo.mapper.UserMapper">
<resultMap id="UserAndRole" type="com.xiumu.securitydemo.model.pojo.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="nickname" property="nickname"/>
<result column="password" property="password"/>
<result column="enabled" property="enabled"/>
<result column="email" property="email"/>
<result column="userface" property="userface"/>
<result column="regTime" property="regTime"/>
<collection property="roles" ofType="com.xiumu.securitydemo.model.pojo.Role">
<id column="rid" property="id"/>
<result column="name" property="name"/>
</collection>
</resultMap>
<sql id="UserAndRole">
u.*,r.id rid,r.name
</sql>
<select id="selectByUserName" parameterType="string" resultMap="UserAndRole">
select <include refid="UserAndRole"/>
FROM user u
LEFT JOIN roles_user ru ON u.id = ru.uid
LEFT JOIN roles r ON ru.rid = r.id
WHERE u.username = #{username}
</select>
</mapper>
多表连接查询就不细说了,大家肯定能看懂。无非就是根据用户名查询一条用户记录。
接下来就是业务层。现在不都是说要面向接口编程嘛,那咱就先建立一个接口,继承UserDetailsService
,为什么继承这个接口呢,了解过的话就会知道,SpringSecurity就是用这个接口的loadUserByUsername
方法来从数据库获取信息,写个类实现这个接口重写方法就完事。
public interface UserService extends UserDetailsService {
}
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return userMapper.selectByUserName(s);
}
}
接下来就是创建控制层了,这个也是简单写两个就行了,就是处理文章管理和用户管理的两个请求。当然在此之前我们还是要写一个通用类,用来当做返回值。
@Data
public class ResultJSON {
/**
* 返回的状态码
*/
private Integer code;
/**
* 返回信息
*/
private String msg;
/**
* 返回的数据
*/
private Object result;
public ResultJSON() {
}
/**
* 只返回状态码
*
* @param code 状态码
*/
public ResultJSON(Integer code) {
this.code = code;
}
/**
* 不返回数据的构造方法
*
* @param code 状态码
* @param msg 信息
*/
public ResultJSON(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
/**
* 返回数据的构造方法
*
* @param code 状态码
* @param msg 信息
* @param result 数据
*/
public ResultJSON(Integer code, String msg, Object result) {
this.code = code;
this.msg = msg;
this.result = result;
}
/**
* 返回状态码和数据
*
* @param code 状态码
* @param result 数据
*/
public ResultJSON(Integer code, Object result) {
this.code = code;
this.result = result;
}
}
接下来开始写正式的controller。
@RestController
public class UserController {
@GetMapping("/hello")
public ResultJSON hello(){
return new ResultJSON(2000,"hello blog!");
}
@GetMapping("/vip")
public ResultJSON vip(){
return new ResultJSON(2000,"hello 超级管理员!");
}
}
普通用户只能访问这个/hello
请求,管理员才能访问这个/vip
请求。
接下来就是重点内容了,如何让SpringSecurity只跟vue返回json数据。而且除了这些,前后端分离也有很多问题,比如跨域问题啊之类的。这些都需要考虑。
对于跨域,我就很暴力了,直接统统允许就完事。且看如下代码:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET","POST")
.maxAge(3600);
}
}
跨域问题解决了,接下来就是SpringSecurity的配置了。我们可以慢慢来,一步步的配置。
首先就是新建一个配置类,继承WebSecurityConfigurerAdapter
类。重写以下两个方法。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/hello").hasRole("普通用户")
.antMatchers("/vip").hasRole("超级管理员")
.anyRequest().authenticated()
.and().cors().and().csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
}
我们看到这有两个方法,有着不同的作用,根据我这几天的半吊子理解,简单的说说
configure(HttpSecurity http)
,这个方法的主要作用就是设置某个请求需要什么权限才能访问,例如这里我设置/hello
由普通用户这个角色访问,/vip
由超级管理员这个角色访问。当然这只是基本的作用,还有很多其他的作用,比如跨域登录请求啊等等。configure(AuthenticationManagerBuilder auth)
,这个方法的主要作用就是设置用户的权限,从数据库获取用户信息之类的。比如这里,我将userService
传给它,它就能自动的从数据库获取用户的信息。到这里并没有结束,接下来我们需要将这些配置一一完善。
新版本里不仅要配置用户的userService,还要配置passwordEncoder,也就是密码的加密解密。既然如此我们就实现一个PasswordEncoder接口好了。当然这也是我参考人家v部落的。代码如下。
public class Md5PasswordEncoderImpl implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
}
/**
* @param charSequence 明文
* @param s 密文
* @return
*/
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(DigestUtils.md5DigestAsHex(charSequence.toString().getBytes()));
}
}
然后把这个实现类配置到上面说的那个configure(AuthenticationManagerBuilder auth)
方法里。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService)
.passwordEncoder(new Md5PasswordEncoderImpl());
}
到这里并没有结束。
接下来就是配置登录请求的url,并允许不登录访问,我就闹过一次笑话,测试的时候没有开启这个不登录访问,结果登录测试居然给我返回我没有登录。
也是一行代码就搞定的事情,在上面说的这个configure(HttpSecurity http)
方法里。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/hello").hasRole("普通用户")
.antMatchers("/vip").hasRole("超级管理员")
.anyRequest().authenticated()
.and().cors().and().csrf().disable()
.formLogin().loginProcessingUrl("/login").permitAll();
}
到这里还没有结束。
这个登录请求url配置完了,那我们登录的时候只需要访问这个url就行了。但是现在还是有问题,就是登录失败会直接给你返回一个登录页面,并不能给你返回一个json数据,我们想要的是它登录失败或者登录成功都应该是一个json数据返回给前端。
这个问题人家自然是考虑到了,所以能够设置登录成功处理与登录失败处理。我们只需要实现人家的登录认证成功接口与登录认证失败接口,再将其配置进去就可以了。
那么我们就需要再写几个类,首先是封装一下登录失败或者登录成功的返回信息,当然我们把没有登录啊,没有权限啊,注销登录的返回信息都封装一下。
public class ResultArgsUtil {
//登录验证失败
public static String USER_NOT_EXIST_FAILURE_MSG = "账号或者密码错误";
public static Integer USER_NOT_EXIST_FAILURE_CODE = 1004;
//没有登录
public static String USER_NOT_LOGIN_FAILURE_MSG = "未登录";
public static Integer USER_NOT_LOGIN_FAILURE_CODE = 1002;
//登录成功
public static String USER_LOGIN_SUCCESS_MSG = "登录成功";
public static Integer USER_LOGIN_SUCCESS_CODE = 1000;
//无权限
public static String AUTHORIZE_FAILURE_MSG = "没有权限";
public static Integer AUTHORIZE_FAILURE_CODE = 1003;
//注销成功
public static String LOGOUT_SUCCESS_MSG = "注销成功";
public static Integer LOGOUT_SUCCESS_CODE = 1005;
}
我们既然要返回json数据,那肯定要设置响应头,把返回对象转成json数据返回给前端。这几个步骤是重复的,唯一不重复的就是返回的对象不一样,所以我们就再封装一个工具类,就是返回json数据的通用方法。
public class SecurityHandlerUtil {
/**
* security处理返回结果
* @param response 响应
* @param result 结果
* @throws IOException
*/
public static void responseHandler(HttpServletResponse response, ResultJSON result) throws IOException {
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(JSONUtil.toJsonStr(result));
writer.flush();
writer.close();
}
}
接下来就是正式写登录认证成功或失败的接口实现类。
public class LoginSuccessHandlerImpl implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
SecurityHandlerUtil.responseHandler(httpServletResponse,new ResultJSON(USER_LOGIN_SUCCESS_CODE,USER_LOGIN_SUCCESS_MSG));
}
}
public class LoginFailureHandlerImpl implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
SecurityHandlerUtil.responseHandler(httpServletResponse,new ResultJSON(USER_NOT_EXIST_FAILURE_CODE,USER_NOT_EXIST_FAILURE_MSG));
}
}
然后再将这两个类配置到上面configure(HttpSecurity http)
这个方法里。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/hello").hasRole("普通用户")
.antMatchers("/vip").hasRole("超级管理员")
.anyRequest().authenticated()
.and().cors().and().csrf().disable()
.formLogin().loginProcessingUrl("/login").permitAll()
.successHandler(new LoginSuccessHandlerImpl())
.failureHandler(new LoginFailureHandlerImpl());
}
这当然还没完。
除了上面那个登录成功与失败处理,这些没有权限啊,未登录啊,注销啊,之类的都需要我们自己来配置一下。所以接下来我们就写一写这些情况下的处理类,当然每个类都要实现人家的接口。
首先是注销的处理类。
public class LogoutHandlerImpl implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
SecurityHandlerUtil.responseHandler(httpServletResponse,new ResultJSON(LOGOUT_SUCCESS_CODE,LOGOUT_SUCCESS_MSG));
}
}
然后是没有权限的处理类。
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
SecurityHandlerUtil.responseHandler(httpServletResponse,new ResultJSON(AUTHORIZE_FAILURE_CODE,AUTHORIZE_FAILURE_MSG));
}
}
接下来就是没有登录的处理类。
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
SecurityHandlerUtil.responseHandler(httpServletResponse,new ResultJSON(USER_NOT_LOGIN_FAILURE_CODE,USER_NOT_LOGIN_FAILURE_MSG));
}
}
然后我们把这些实现类都配置到上面configure(HttpSecurity http)
这个方法里。如此才是全部的配置。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/hello").hasRole("普通用户")
.antMatchers("/vip").hasRole("超级管理员")
.anyRequest().authenticated()
.and().cors().and().csrf().disable()
.formLogin().loginProcessingUrl("/login").permitAll()
.successHandler(new LoginSuccessHandlerImpl())
.failureHandler(new LoginFailureHandlerImpl())
.and()
.logout().logoutSuccessHandler(new LogoutHandlerImpl()).permitAll()
.and()
.exceptionHandling()
.accessDeniedHandler(new AccessDeniedHandlerImpl())
.authenticationEntryPoint(new AuthenticationEntryPointImpl());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService)
.passwordEncoder(new Md5PasswordEncoderImpl());
}
}