【Java】Spring Security 简单上手

1、引入依赖

    <!--  spring-srcrity 安全框架-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
	<!--验证码-->
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
        </dependency>

2、编写配置类OaSecurityConfig

package com.manager.oa.config;

import com.manager.oa.filter.CaptchaFilter;
import com.manager.oa.filter.ValidateException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Author sms
 * @Version V1.0.0
 * @Date 2022-08-17
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用权限验证
public class OaSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 配置验证码过滤器
     *
     * @return
     */
    @Bean
    public CaptchaFilter captchaFilter() {
        return new CaptchaFilter();
    }

    /**
     * spring security 配置类
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 验证码过滤器,加在了用户密码过滤器之前
        http.addFilterBefore(captchaFilter(), UsernamePasswordAuthenticationFilter.class);
        http.formLogin()
                .loginPage("/portal/login") //指定自己的登录页面
                .loginProcessingUrl("/portal/tologin")  //与登录表单的action保持一致
                // .successForwardUrl("/portal/")
                .successHandler(new AuthenticationSuccessHandler() {  // 成功定制
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        response.sendRedirect("/portal/"); // 从定向
                        //  response.getWriter().print(""); // 返回前端JSON
                    }
                })
                // .failureForwardUrl("/portal/login") // 失败后的url
                .failureHandler(authenticationFailureHandler()) // 失败后进行异常处理
                .usernameParameter("account")
                .passwordParameter("password")
                .and()
                .authorizeRequests() //下面的都是授权的配置 3
                .antMatchers("/portal/login", "/css/**", "/images/*", "/captcha").permitAll()
                .anyRequest() //任何请求 4
                .authenticated()//要求认证
                .and().csrf().disable(); //禁用csrf攻击解决方案

        http.headers().frameOptions().disable()
                .and()
                .logout();
        // 在没有权限访问时,SpriSecurity会抛出AccessDeniedException,为了处理该异常,使用以下配置
        http.exceptionHandling().accessDeniedHandler((req, resp, e) -> {
            resp.sendRedirect("/portal/noperm");
        });
        // 在登录成功之后,如果会话失效(默认30分钟或手动删除Cookie),默认跳到登录界面,如果需要自定义,
        http.exceptionHandling().authenticationEntryPoint((req, resp, e) -> {
            resp.sendRedirect("/portal/nosession");
        });

    }

    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 异常处理Bean的配置
     * 由于框架在验证密码和用户名时,做了相关的模糊处理,默认提示“用户名或密码错误”,
     * 需要将HideUserNotFoundExceptions设置为false,就可以分别提示,用户名,密码错误;
     * <p>
     * 由于 new DaoAuthenticationProvider();之后,里面的属性都为空,需要重新将userDetailsService 设置进去
     *
     * @return
     */
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        // 为了提供细致提示,设置Provider的hideUserNotFoundExceptions的属性为false
        daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        return daoAuthenticationProvider;
    }

    /**
     * 异常处理Bean
     *
     * @return
     */
    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler() {
        return (req, resp, exception) -> {
            if (exception instanceof UsernameNotFoundException) {
                req.setAttribute("msg", "账号不存在");
            } else if (exception instanceof BadCredentialsException) {
                req.setAttribute("msg", "密码不正确");
            } else if (exception instanceof ValidateException) {
                req.setAttribute("msg", "验证码不正确");
            } else {
                req.setAttribute("msg", "系统维护中");
            }
            req.getRequestDispatcher("/portal/login").forward(req, resp);
        };
    }

    /**
     * 密码加密
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

3、编写自己的OaUserDetailsService,实现框架的UserDetailsService
如下:
框架中的UserDetailsService,里面只有一个方法,功能是通过用户名来验证用户;
如果找不到用户,或者用户不存在,则抛出UsernameNotFoundException
所以我们需要实现该方法首先验证用户名是否正确
在这里插入图片描述
由于该方法需要返回一个UserDetails(框架中的);
该UserDetails主要存在以下属性,显然,这些都是一些基本属性,由于我们还要做权限验证,这些属性是不够的,
在这里插入图片描述
在这里插入图片描述
所以我们需要编写自己的user类,来实现该类,增强一些属性:
编写:OaUser

package com.manager.oa.pojo.oa;

import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

/**
 * @Author sms
 * @Version V1.0.0
 * @Date 2022-08-18
 */
@Getter
@Setter
public class OaUser extends User {
    public OaUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
    }

    public OaUser(String username, String password, com.manager.oa.pojo.rbac.User user, Collection<? extends GrantedAuthority> authorities) {
        this(username, password, authorities);
        this.user = user;
    }
	// 新增属性,返回自己的整个实体类中的所有属性
    private com.manager.oa.pojo.rbac.User user;
}

由于麻烦。我直接新增了我自己的use实体类
以下是我的User

@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(value = {"handler"})
public class User {
    private Integer id;
    private String account; // 账号
    private String password; // 密码
    private String img;// 头像
    private Role role;// 角色
    private String state;// 状态
    private Integer roleId;// 角色ID
}

现在编写OaUserDetailsService,重新 loadUserByUsername 该方法返回的就是我们刚刚编写的OaUser 该类是实现了框架的UserDetails的;
返回的参数作简要说明:
第一个参数:username,就是我们需要验证的账号,也就是前端传过来的用户名
第二个参数:password, 为数据库中查询到的该账号的密码
第三个参数:data, 为需要返回的数据,(我这里直接返回数据库中user的实体类)
第四个参数:list,为该用户的所有权限,为后面做权限验证准备

package com.manager.oa.service.rbac.impl;

import com.manager.oa.mapper.rbac.UserMapper;
import com.manager.oa.pojo.oa.OaUser;
import com.manager.oa.pojo.rbac.Permission;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @Author sms
 * @Version V1.0.0
 * @Date 2022-08-17
 */
@Slf4j
@Component
public class OaUserDetailsService implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据账号去查询用户信息,把用户的权限查出来
        log.debug("用户账号:{}", username);
        com.manager.oa.pojo.rbac.User user = new com.manager.oa.pojo.rbac.User();
        user.setAccount(username);
        List<com.manager.oa.pojo.rbac.User> users = userMapper.findUsersByCondition(user);
        if (users.size() == 0) {//避免空指针
            throw new UsernameNotFoundException("账号不存在");    //抛出异常
        }
        log.debug("数据库信息:{}", users.get(0));
        log.debug("用户权限信息:{}", users.get(0).getRole().getPermissions());
        List<SimpleGrantedAuthority> list = new ArrayList<>();
        for (Permission p : users.get(0).getRole().getPermissions()
        ) {
            //过滤父权限
            if (p.getPerPower() == null) {
                continue;
            }
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(p.getIdentify());
            list.add(simpleGrantedAuthority);
        }
        return new OaUser(username, users.get(0).getPassword(), users.get(0), list);
    }
}

以下是我的 UserMapper,由于我开始编写了一个多条件查询的方法,所以我直接用的是多条件查询,也可以写一个通过用户名查找信息的方法;

/**
 * 多条件查询
 *
 * @param user
 * @return
 */
@SelectProvider(type = UserSqlProvider.class, method = "select")
@Results(
        {
                @Result(column = "id", property = "id", id = true),
                @Result(column = "account", property = "account"),
                @Result(column = "password", property = "password"),
                @Result(column = "img", property = "img"),
                @Result(column = "state", property = "state"),
                @Result(column = "role", property = "role", one = @One(select = "com.manager.oa.mapper.rbac.RoleMapper.findRoleById", fetchType = FetchType.LAZY)),
        }
)
List<User> findUsersByCondition(User user);

然后是Role角色实体类:

@Data // get set toString equals 等
@AllArgsConstructor // 带参构造
@NoArgsConstructor // 无参构造
public class Role {
    private Integer id;
    private String name; //名称
    private String description;//描述
    private String state; // 状态
    private List<Permission> permissions; //权限集合
    private List<Menu> menus; // 菜单集合
}

RoleMapper中的用到的方法:

/**
 * 通过ID查询
 *
 * @param id
 * @return
 */
@Select("select * from rbac_roles where id=#{id}")
@Results({
        @Result(column = "id",property = "id",id = true),
        @Result(column = "id", property = "permissions",many = @Many(select = "com.manager.oa.mapper.rbac.PermissionMapper.findByRoleId")),
        @Result(column = "id", property = "menus",many = @Many(select = "com.manager.oa.mapper.rbac.MenuMapper.findByRoleId")),

})
Role findRoleById(int id);

其次是该查询方法中的查询权限和菜单的方法:

   /**
     * 通过角色ID查询权限
     *
     * @param roleId
     */
    @Select("SELECT * from rbac_permission p,rbac_role_power r WHERE p.id=r.power and role=#{roleId}")
    List<Permission> findByRoleId(int roleId);
   /**
     * 通过角色ID查询菜单
     *
     * @param roleId
     */
    @Select("SELECT * from rbac_menus m,rbac_role_menu r WHERE m.id=r.menu and role=#{roleId} and state='y'")
    List<Menu> findByRoleId(int roleId);

完成上面我们就对前端传过来的账号密码做了验证,但是还有验证码没有验证,所以还需要编写验证码过滤器,该过滤器是放在账号密码过滤器之前的,
也就是OaSecurityConfig中的这句话
在这里插入图片描述

 // 验证码过滤器,加在了用户密码过滤器之前
        http.addFilterBefore(captchaFilter(), UsernamePasswordAuthenticationFilter.class);

所以还需要编写验证码过滤器
CaptchaFilter

package com.manager.oa.filter;

import com.google.code.kaptcha.Constants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 验证码过滤器
 *
 * @Author sms
 * @Version V1.0.0
 * @Date 2022-08-19
 */
public class CaptchaFilter extends OncePerRequestFilter {
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if ("/portal/tologin".equals(request.getRequestURI()) && "post".equalsIgnoreCase(request.getMethod())) {
            String code = request.getParameter("code");
            String captcha = request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY).toString();
            if (!code.equalsIgnoreCase(captcha)) {
                // 认证失败
                authenticationFailureHandler.onAuthenticationFailure(request, response, new ValidateException("验证码错误"));
                return;
            }
        }
        filterChain.doFilter(request, response);
    }
}

验证码错误自定义异常

/**
 * 验证码错误异常
 *
 * @Author sms
 * @Version V1.0.0
 * @Date 2022-08-20
 */
public class ValidateException extends AuthenticationException {
    public ValidateException(String msg, Throwable cause) {
        super(msg, cause);
    }

    public ValidateException(String msg) {
        super(msg);
    }
}

这里的验证码用的是kaptcha
产生验证码的配置类:

package com.manager.oa.config;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

import static com.google.code.kaptcha.Constants.*;

/**
 * 图片验证码属性配置类
 *
 * @Author sms
 * @Version V1.0.0
 * @Date 2022-08-19
 */
@Configuration
public class CaptchaConfig {
    @Bean
    public DefaultKaptcha defaultKaptcha() {
        Properties properties = new Properties();
        // 是否有边框 默认为true 我们可以自己设置yes,no
        properties.setProperty("kaptcha.border", "yes");
        // 验证码文本字符颜色 默认为Color.BLACK
        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
        // 验证码图片宽度 默认为200
        properties.setProperty(KAPTCHA_IMAGE_WIDTH, "100");
        // 验证码图片高度 默认为50
        properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "34");
        // 验证码文本字符大小 默认为40
        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "30");
        // KAPTCHA_SESSION_KEY
        properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "captcha");
        // 验证码文本字符长度 默认为5
        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
        // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial");
        // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
        properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.WaterRipple");
        Config config = new Config(properties);

        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}

在这里插入图片描述

编写验证码的CaptchaController

package com.manager.oa.controller.portal;

import com.google.code.kaptcha.Constants;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.image.BufferedImage;
import java.io.IOException;

/**
 * 图片验证码
 *
 * @Author sms
 * @Version V1.0.0
 * @Date 2022-08-19
 */
@Controller
public class CaptchaController {
    @Autowired
    private DefaultKaptcha defaultKaptcha;

    @GetMapping(value = "/captcha")
    public void getKaptchaImage(HttpServletRequest request, HttpServletResponse response) {
        ServletOutputStream out = null;
        try {
            response.setDateHeader("Expires", 0);
            response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
            response.addHeader("Cache-Control", "post-check=0, pre-check=0");
            response.setHeader("Pragma", "no-cache");
            response.setContentType("image/jpeg");
            //生成验证码
            String capText = defaultKaptcha.createText();
            HttpSession session = request.getSession();
            session.setAttribute(Constants.KAPTCHA_SESSION_KEY, capText);
            //向客户端写出
            BufferedImage bi = defaultKaptcha.createImage(capText);
            out = response.getOutputStream();
            ImageIO.write(bi, "jpg", out);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

最后:LoginController

@Controller("portalLoginController")
@RequestMapping("/portal")
public class LoginController {

    @RequestMapping("/login")
    public String login() {
        return "login";
    }

}

最后,我的登录界面

<!DOCTYPE html>
<html lang="zh-cn" xmlns:th="http://www.thymeleaf.org" xmlns:ch="http://www.thymeleaf.org">
<head>
<title>登录</title>
<link rel="stylesheet" th:href="@{/css/pintuer.css}">
<link rel="stylesheet" th:href="@{/css/admin.css}">
<script type="text/javascript">
	
</script>
</head>
<body>
	<div class="bg"></div>
	<div class="container">
		<div class="line bouncein">
			<div class="xs6 xm4 xs3-move xm4-move">
				<div style="height:150px;"></div>
				<div class="media media-y margin-big-bottom"></div>
				<form action="/portal/tologin" method="post">

					<div class="panel loginbox">
						<div class="text-center margin-big padding-big-top">
							<h1>后台管理中心</h1>
							<ch:block ch:text="${msg}"></ch:block>
						</div>
						<div class="panel-body"
							style="padding:30px; padding-bottom:10px; padding-top:10px;">
							<div class="form-group">
								<div class="field field-icon-right">
<!--									<input type="hidden" name="_csrf" th:value="${_csrf.getToken()}">-->
									<input type="text" class="input input-big" name="account"
										placeholder="登录账号" value="admin3"/> <span
										class="icon icon-user margin-small"></span>
								</div>
							</div>
							<div class="form-group">
								<div class="field field-icon-right">
									<input type="password" class="input input-big" name="password"
										placeholder="登录密码" value="123123" /> <span
										class="icon icon-key margin-small"></span>
								</div>
							</div>
							<div class="form-group">
								<div class="field">
									<input type="text" class="input input-big" name="code"
										   placeholder="填写右侧的验证码" style="width: 225px;display: inline"/>
									<img src="/captcha"
										 alt="" width="100" height="44" onclick="this.src = '/captcha?t=' + new Date().getTime();">
								</div>
							</div>
						<div style="padding:30px;">
							<input type="submit"
								class="button button-block bg-main text-big input-big"
								value="登录">
						</div>
					</div>
				</form>
			</div>
		</div>
	</div>
</body>
</html>

最后需要用到的测试类,及html
权限验证

 @PreAuthorize("hasAuthority('dept:list')") // 需要的权限

这个就是最开始从数据库中查询到的权限

@Controller("portalDeptController")
@RequestMapping("/dept")
public class DeptController {
    @PreAuthorize("hasAuthority('dept:list')") // 需要的权限
    @GetMapping("/list")
    public String list(){
        return "dept_list";
    }
}

NoPermController 权限不足,会话过期controller

package com.manager.oa.controller.portal;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @Author sms
 * @Version V1.0.0
 * @Date 2022-08-22
 */
@Controller("/portal")
public class NoPermController {
    /**
     * 没有权限
     *
     * @return
     */
    @RequestMapping("/noperm")
    public String noPerm() {
        return "noperm";
    }

    /**
     * 会话过期
     *
     * @return
     */
    @RequestMapping("/nosession")
    public String noSession() {
        return "nosession";
    }
}

noperm.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
权限不足
</body>
</html>

nosession.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<a href="/portal/login" target="_top">登录</a>
</body>
</html>

在这里插入图片描述

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值