本文是在超全的springboot+springsecurity前后端分离简单实现!!!_码上编程的博客-CSDN博客 此文的基础上进一步改进的,
没有接触过springsecurity的可以先看看超全的springboot+springsecurity前后端分离简单实现!!!_码上编程的博客-CSDN博客,两篇文章都是每一步都有详细步骤!
1、前言部分
1.1 序言
我的上一篇博客超全的springboot+springsecurity前后端分离简单实现!!!_码上编程的博客-CSDN博客 虽然实现了前后端分离,但是它是基于cookie认证的,我想尝试一下不用cookie认证,而是用token认证。网上绝大部分都是使用 jwt(json web token)方式认证,而jwt加密和解密比较繁琐,现实当中好像用的挺少的,但是不可否认它更安全,如果想看基于jwt方式认证的,我推荐此博客SpringBoot整合SpringSecurity+JWT实现用户验证和鉴权_西瓜不甜柠檬不酸的博客-CSDN博客,但这篇还是使用token认证,将token保存在redis缓存中。
源代码还有数据库在此文的底部!!
1.2 目标实现效果
注册, 密码必须经过BCryptPasswordEncoder进行加密,权限必须以ROLE_开头才能被springsecurity识别
账号密码错误,登录失败 。 /login接口有springsecurity写好了,直接调用即可,但是必须使用post请求, 参数名必须是username和password,要不然登录不成功。
登录成功 , 并在响应头中返回token
权限不足访问目标资源, /hello是我写的一个接口,返回"hello"字符串。
权限匹配访问目标资源, /index 是我写的一个接口,返回"index"字符串
注销操作, /logout是springsecurity自动封装的接口,无论是get请求还是post请求,直接调用即可, 注销成功后会删除缓存里的token
1.3 技术使用
springboot、mybatis-plus、redis、mysql、lombok自动生成get/set、git
2、关键部分
2.1 原理
2.2 代码部分
<dependencies>
<!--转换成json字符串的工具-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.2</version>
</dependency>
<!-- 配置使用redis启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</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>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<!--模板引擎-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.2</version>
</dependency>
<!--自动生成代码时会用到的依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.4.2</version>
</dependency>
</dependencies>
User.java
@Data
@EqualsAndHashCode(callSuper = false)
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String username;
private String password;
private String role;
}
UserMapper.java, BaseMapper<T>封装了大量的sql语句供程序员使用
@Repository
public interface UserMapper extends BaseMapper<User> {
}
UserService.java
public interface UserService extends IService<User> {
}
Msg.java 自定义结果返回集
/**
* 自定义结果集处理
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Msg {
int code;
String message;
Object data;
//无权访问
public static Msg denyAccess(String message){
Msg result=new Msg();
result.setCode(300);
result.setMessage(message);
return result;
}
//操作成功
public static Msg success(String message){
Msg result=new Msg();
result.setCode(200);
result.setMessage(message);
return result;
}
//客户端操作失败
public static Msg fail(String message){
Msg result=new Msg();
result.setCode(400);
result.setMessage(message);
return result;
}
}
注销处理器
@Component
public class AuthenticationLogout implements LogoutSuccessHandler {
@Autowired
Gson gson;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String token = request.getHeader("token");
Msg result=null;
try {
if(token==null){
//token为空表示未登录,注销失败
result=Msg.fail("未登录,不能进行注销操作!!!");
}else{
String username = stringRedisTemplate.opsForValue().get(token);
if(username==null){
//token不正确,注销失败
result=Msg.fail("登录凭证异常,注销失败!!!");
}else{
//token正确,注销成功
result=Msg.success("注销成功");
//清空token
stringRedisTemplate.delete(token);
}
}
}catch (Exception e){
e.printStackTrace();
}
response.setContentType("application/json;charset=utf-8"); //设置编码格式
response.getWriter().write(gson.toJson(result)); //返回给前端
}
}
未登录时处理器
/**
* 未登录时处理器
*/
public class TokenAuthenticationEntryPoint implements AuthenticationEntryPoint {
Gson gson=new Gson();
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
Msg msg= Msg.fail("请登录!!");
response.getWriter().write(gson.toJson(msg));
}
}
权限不足处理器
/**
* 权限不足处理器
*/
public class TokenAccessDeniedHandler implements AccessDeniedHandler {
Gson gson=new Gson();
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
Msg msg= Msg.denyAccess("权限不够,请联系管理员!!!");
response.getWriter().write(gson.toJson(msg));
}
}
请求过滤器 , token没有或者不正确的时候, 告诉用户执行相应操作,token正确且未认真的情况下则放行请求, 交由认证过滤器进行认证操作
/**
* 自定义请求过滤器,token没有或者不正确的时候,
* 告诉用户执行相应操作,token正确且未认真的情况下则放行请求,
* 交由认证过滤器进行认证操作
*/
public class OncePerRequestAuthoricationFilter extends BasicAuthenticationFilter {
StringRedisTemplate stringRedisTemplate;
UserServiceImpl userServiceImpl;
Gson gson=new Gson();
public OncePerRequestAuthoricationFilter(AuthenticationManager authenticationManager, StringRedisTemplate stringRedisTemplate, UserServiceImpl userServiceImpl) {
super(authenticationManager);
this.stringRedisTemplate=stringRedisTemplate;
this.userServiceImpl=userServiceImpl;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String token=request.getHeader("token");
if(token==null || token.equals("")){
//token为空,则返回空
chain.doFilter(request, response);
return;
}
String username=stringRedisTemplate.opsForValue().get(token);
try{
//判断token情况,给予对应的处理方案
if(username==null){
throw new Exception("登录凭证不正确或者超时了,请重新登录!!!");
}else{
UserDetails userDetails = userServiceImpl.loadUserByUsername(username);
if(userDetails!=null){
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(username,null,userDetails.getAuthorities());
response.setHeader("token",token);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
}catch (Exception e){
//抛出异常,并返回给前端
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=utf-8");
Msg msg= Msg.fail(e.getLocalizedMessage());
response.getWriter().write(gson.toJson(msg));
response.getWriter().flush();
chain.doFilter(request, response);
return;
}
super.doFilterInternal(request,response,chain);
}
}
认证过滤器, 判断认证成功还是失败,并给予相对应的逻辑处理
/**
* 自定义认证过滤器,判断认证成功还是失败,并给予相对应的逻辑处理
*/
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
AuthenticationManager authenticationManager;
StringRedisTemplate stringRedisTemplate;
Gson gson=new Gson();
public AuthenticationFilter(AuthenticationManager authenticationManager,StringRedisTemplate stringRedisTemplate){
this.authenticationManager=authenticationManager;
this.stringRedisTemplate=stringRedisTemplate;
}
//未认证时调用此方法,判断认证是否成功,认证成功与否由authenticationManager.authenticate()去判断,我们在这里只负责传递所需要的参数即可
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username=request.getParameter("username");
String password=request.getParameter("password");
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username,password,new ArrayList<>()));
}
//验证成功操作
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
/**
* 验证成功则向redis缓存写入token,然后在响应头添加token,并向前端返回
*/
String token= UUID.randomUUID().toString().replaceAll("-",""); //token本质就是随机生成的字符串
stringRedisTemplate.opsForValue().set(token,request.getParameter("username"),60*10,TimeUnit.SECONDS); //存入缓存中
response.setHeader("token",token); //在响应头添加token
Msg msg= Msg.success("登录成功");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.getWriter().write(gson.toJson(msg));
}
//验证失败
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
/**
* 验证成功则向前端返回失败原因
*/
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
Msg msg=Msg.fail("账号或者密码错误");
response.getWriter().write(gson.toJson(msg));
}
}
springsecurity核心配置文件,无论是处理器还是过滤器都需要注入到此
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//java操作redis的string类型数据的类
@Autowired
StringRedisTemplate stringRedisTemplate;
//注销处理器
@Autowired
AuthenticationLogout authenticationLogout;
//加密
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
return new UserServiceImpl();
}
/**
* 认证
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(bCryptPasswordEncoder());
}
/**
* 授权
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
.anyRequest().permitAll()
.and()
.logout()
.permitAll()
.logoutSuccessHandler(authenticationLogout) //注销时的逻辑处理
.and()
.addFilter(new AuthenticationFilter(authenticationManager(),stringRedisTemplate)) //自定义认证过滤器
.addFilter(new OncePerRequestAuthoricationFilter(authenticationManager(),stringRedisTemplate, (UserServiceImpl) userDetailsService())) //自定义请求过滤器
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //去除默认的session、cookie
.and()
.exceptionHandling().authenticationEntryPoint(new TokenAuthenticationEntryPoint()) //未登录时的逻辑处理
.accessDeniedHandler(new TokenAccessDeniedHandler()); //权限不足时的逻辑处理
}
/**
* 用于解决跨域问题
* @return
*/
@Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
UserServiceImpl.java, 实现UserDetailsService里面的loadUserByUsername()方法,AuthenticationManager会调用此方法去获取用户数据信息,从而完成认证。
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService,UserDetailsService {
@Autowired
UserMapper userMapper;
/**
* 实现UserDetailsService接口的方法,用于获取用户个人信息
* @param s
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//根据用户名查找用户,相当于select * from user where username='${s}'
QueryWrapper<User> wrapper=new QueryWrapper<>();
wrapper.eq("username",s);
User user = userMapper.selectOne(wrapper);
if(user==null){
throw new UsernameNotFoundException("用户名错误!!");
}
//获取用户权限,并把其添加到GrantedAuthority中
List<GrantedAuthority> grantedAuthorities=new ArrayList<>();
GrantedAuthority grantedAuthority=new SimpleGrantedAuthority(user.getRole());
grantedAuthorities.add(grantedAuthority);
return new org.springframework.security.core.userdetails.User(s,user.getPassword(),grantedAuthorities);
}
/**
* 注册操作
* @param user
* @return
*/
public Msg register(User user){
user.setPassword(new BCryptPasswordEncoder().encode(user.getPassword())); //对密码进行加密
int insert = userMapper.insert(user);
if(insert>0){
return Msg.success("注册成功!");
}else{
return Msg.fail("注册失败!");
}
}
}
UserController.java , @PreAuthorize("hasRole('ROLE_USER')") 指定接口拥有ROLE_USER的权限方可访问的注解。
@RestController
public class UserController {
@Autowired
UserServiceImpl userServiceImpl;
/**
* 注册操作
* @param user
* @return
*/
@PostMapping("/register")
public Msg register(User user){
return userServiceImpl.register(user);
}
/**
* 当权限为ROLE_ADMIN时方可访问,否则抛出权限不足异常
* @return
*/
@GetMapping("/index")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public Msg index(){
Msg msg=Msg.success("查询成功!!!");
msg.setData("index");
return msg;
}
/**
* 当权限为ROLE_USER时方可访问,否则抛出权限不足异常
* @return
*/
@GetMapping("/hello")
@PreAuthorize("hasRole('ROLE_USER')")
public Msg hello(){
Msg msg=Msg.success("查询成功!!!");
msg.setData("hello");
return msg;
}
}
项目源代码地址: https://gitee.com/liu-wenxin/springsecurity_token.git , 通过 git clone https://gitee.com/liu-wenxin/springsecurity_token.git 获取源代码以及数据库文件。