一、
二、Spring Security
Spring Security的认证流程可参考https://blog.csdn.net/u013435893/article/details/79605239
1.基本环境搭建
1.1导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
1.2.建立一个web层请求接口(非必须)
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping
public String getUsers() {
return "Hello Spring Security";
}
}
1.3运行并访问请求接口
我们知道,在未添加Spring Security前,在浏览器访问http://localhost:8080/user或http://localhost:8080,会在页面中显示Hello Spring Security;添加该权限框架的依赖后,此时再次访问,无法正常访问,弹出了身份验证(登录)页面,
其原因是在Springboot中,Spring Security默认是生效的,当添加了该依赖后,所有的接口(访问路径)都是受保护的,需要验证才能正常访问,Spring Security提供了一个默认的用户,用户名是user,而密码则是启动项目的时候自动生成的。也可通过配置文件修改默认的用户名和密码。查看项目启动的日志,会发现如下的一段Log
直接用user和启动日志中的密码登录即可正常调用接口了。
其中的登录界面是Spring Security框架提供的默认页面,被称为HttpBasicLogin。
2.自定义用户认证逻辑
在实际生产中,不会使用默认的认证逻辑和登录页面,需根据实际项目来定义其逻辑和页面。
2.1.配置用户认证逻辑
Spring Security默认的认证逻辑是UserDetailsService,在实际生产项目中,我们需要有自己的认证逻辑,则可实现该接口。
@Service("userDetailsService")
public class CustomUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库中取出用户信息
// 判断用户是否存在
// 封装用户信息,并返回。参数分别是:用户名,密码,用户权限
return new User("zhangsan", new BCryptPasswordEncoder(4).encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
2.1.1UserDetails
在上面认证逻辑中返回的对象就是UserDetails,该类封装了用户信息的对象,里面包含了七个方法,在返回该对象的时候,也可通过调用其实现类的构造方法设置值。
public interface UserDetails extends Serializable {
// 封装了权限信息
Collection<? extends GrantedAuthority> getAuthorities();
// 密码信息
String getPassword();
// 登录用户名
String getUsername();
// 帐户是否过期
boolean isAccountNonExpired();
// 帐户是否被冻结
boolean isAccountNonLocked();
// 帐户密码是否过期,一般有的密码要求性高的系统会使用到,比较每隔一段时间就要求用户重置密码
boolean isCredentialsNonExpired();
// 帐号是否可用
boolean isEnabled();
}
2.1.2密码加密解密
SpringSecurity中有一个PasswordEncoder接口,在实际生产中,我们只需实现该接口,并配置到容器中即可。默认为我们提供了一个实现类BCryptPasswordEncoder,我们只需将其注入到容器中。
2.2.Spring Security相关配置
创建配置类
@Configuration //指明该类是一个配置类
@EnableWebSecurity //开启 Security 服务
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启全局 Securtiy 注解,开启方法权限控制
@Slf4j
// 核心配置,配置SpringSecurity访问策略,包括登录处理,登出处理,资源访问,密码基本加密。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//配置策略
@Override
protected void configure(HttpSecurity http) throws Exception {
// post请求默认的都开启了csrf的模式,所有post请求都必须带有token之类的验证信息才可以进入登陆页面,此处是禁用csrf模式
http.csrf().disable();
http.formLogin() // 定义当需要用户登录时候,转到的登录页面。
.and()
.authorizeRequests() // 定义哪些URL需要被保护、哪些不需要被保护
.anyRequest() // 任何请求,登录后可以访问
.authenticated();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() { //密码加密
return new BCryptPasswordEncoder(4);
}
}
2.3访问
根据UserDetailsService实现类中设定的用户名和密码登录测试成功。
3.个性化认证逻辑
3.1.配置用户认证逻辑
@Service("userDetailsService")
public class CustomUserDetailService implements UserDetailsService {
@Autowired
private SysUserService userService;
@Autowired
private SysRoleService roleService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Collection<GrantedAuthority> authorities = new ArrayList<>();
// 从数据库中取出用户信息
SysUser user = userService.selectByName(username);
// 判断用户是否存在
if(user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
//根据用户获取其权限信息
List<SysUserRole> userRoles = userRoleService.listByUserId(user.getId());
for (SysUserRole userRole : userRoles) {
SysRole role = roleService.selectById(userRole.getRoleId());
authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
}
// 返回UserDes实现类user
return new User(user.getUsername(), user.getPassword(), authorities);
}
}
3.2.Spring Security相关配置
创建配置类,
- 继承
WebSecurityConfigurerAdapter
,并重写它的方法来设置一些web安全的细节
@Configuration //指明该类是一个配置类
@EnableWebSecurity //开启 Security 服务
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启全局 Securtiy 注解,开启方法权限控制
@Slf4j
// 核心配置,配置SpringSecurity访问策略,包括登录处理,登出处理,资源访问,密码基本加密。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//
@Autowired
CustomUserDetailService userDetailService;
//配置策略
/** 放行静态资源 */
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/static/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// post请求默认的都开启了csrf的模式,所有post请求都必须带有token之类的验证信息才可以进入登陆页面,此处是禁用csrf模式
http.csrf().disable();
http.authorizeRequests() // 定义哪些URL需要被保护、哪些不需要被保护
.antMatchers("/static/**").permitAll() // 所有用户均可访问的资源
.anyRequest() // 任何尚未匹配的URL只需要验证用户即可访问
.authenticated()
.and()
.formLogin() // 定义当需要用户登录时候,转到的登录页面。
.loginPage("/login").permitAll() // 设置登录页面
.successHandler(loginSuccessHandler()) //自定义的登录接口
.and()
.logout().permitAll().invalidateHttpSession(true) //退出清空session
.deleteCookies("JSESSIONID").logoutSuccessHandler(logoutSuccessHandler())
.and()
.sessionManagement().maximumSessions(10).expiredUrl("/login");
//.usernameParameter("username") // 自定义登陆用户名和密码参数,默认为username和password
//.passwordParameter("password")
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
auth.eraseCredentials(false);
}
@Bean
public BCryptPasswordEncoder passwordEncoder() { //密码加密
return new BCryptPasswordEncoder(4);
}
@Bean
public LogoutSuccessHandler logoutSuccessHandler() { //登出处理
return new LogoutSuccessHandler() {
//第三个参数AuthenticationException,包括了登录失败的信息
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
try {
SecurityUser user = (SecurityUser) authentication.getPrincipal();
log.info("USER : " + user.getUsername() + " LOGOUT SUCCESS ! ");
} catch (Exception e) {
log.info("LOGOUT EXCEPTION , e : " + e.getMessage());
}
httpServletResponse.sendRedirect("/login");
}
};
}
@Bean
public SavedRequestAwareAuthenticationSuccessHandler loginSuccessHandler() { //登入处理
return new SavedRequestAwareAuthenticationSuccessHandler() {
//该方法的第三个参数包含了登录用户的信息(UserDetails),Session信息,登录信息等
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
SysUser userDetails = (SysUser) authentication.getPrincipal();
log.info("USER : " + userDetails.getUsername() + " LOGIN SUCCESS ! ");
super.onAuthenticationSuccess(request, response, authentication);
}
};
}
}
其中需注意,静态资源的访问不仅需要在此处配置,还需要添加如下配置
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
}
}
3.3自定义登录页面
通过设置loginPage(""),可以是指定页面直接跳转或指定路径通过控制层返回。
3.4自定义处理登录成功/失败
在传统开发中,登录成功了都是进行了页面的跳转。
在前后端分离的情况下,我们登录成功了可能需要向前端返回用户的个人信息,而不是直接进行跳转。登录失败也是同样的道理。这里涉及到了Spring Security中的两个接口AuthenticationSuccessHandler和AuthenticationFailureHandler。我们可以实现这个接口,并进行相应的配置就可以了。 当然框架是有默认的实现类的,我们可以继承这个实现类再来自定义自己的业务,在前面的配置类中我们就定义了匿名内部类的方式实现。
4.验证码
4.1图形验证码
SpringSecurity是通过过滤器链来进行校验的,我们想要验证图形验证码,所以可以在认证流程之前,也就是UsernamePasswordAuthenticationFilter
之前进行校验。
4.1.1图形验证码整体流程大概是:
1).页面初始化的时候服务器生成一个验证码,然后将验证码保存到session中,再显示给html(客户端)
2).整合springsecurity校验,自定义一个filter,将该filter设置在UsernamePasswordAuthenticationFilter之前执行,这样就会在验证用户名密码之前就校验验证码
3).我们自定义的filter里面校验html传来的验证码和第二步我们保存到session的验证码是否相同,如果相同那么就放行,验证成功
4.1.2具体实现
1.页面初始化的时候服务器生成一个验证码,然后将验证码保存到session中,再显示给html(客户端)
a.验证码信息
@Data
public class ImageCode {
private BufferedImage image;
private String code;
private LocalDateTime expireTime;//过期时间
/**
* @param image
* @param code
* @param expireInt :该参数是过期时间秒数,如60
*/
public ImageCode(BufferedImage image, String code, int expireInt) {
this.image = image;
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireInt);//当前时间加上60秒
}
}
b.验证码工具类
public class ImageCodeUtil{
public static ImageCode createImageCode() {
int width=80;
int height=30;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
String code = "";
for (int i = 0; i < 4; i++) {
String rand = String.valueOf(random.nextInt(10));
code += rand;
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 16);
}
g.dispose();
return new ImageCode(image, code, 60);
}
/**
* 生成随机背景条纹
* @param fc
* @param bc
* @return
*/
private static Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
}
c.请求生成验证码(ImageCode)对象
@RestController
public class ImageCodeController {
@RequestMapping(value = "/imageCode", method = RequestMethod.GET)
public void imageCode(HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException {
//生成imageCode对象
ImageCode imageCode = ImageCodeUtil.createImageCode();
//将图形验证码存入到session中
request.getSession().setAttribute("imageCode", imageCode);
// 将生成的图片写到接口的响应中
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}
}
2.整合springsecurity校验,自定义一个filter,将该filter设置在UsernamePasswordAuthenticationFilter之前执行,这样就会在验证用户名密码之前就校验验证码
a.自定义过滤器filter
@Component
public class ValidateCodefilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String uri= request.getRequestURI();
//如果为get请求并且请求uri为/login(也就是我们登录表单的form的action地址)
if( StringUtils.equalsIgnoreCase(request.getMethod(),"get") && StringUtils.containsIgnoreCase(request.getRequestURI(),"/login")) {
logger.info("ValidateCodefilter执行了----" + "request.getRequestURI()=" + uri);
//这里需要验证前端传过来的验证码是否和session里面存的一致,并且要判断是否过期
logger.info(request.getSession().getAttribute("imageCode"));
validateCode(request);
}
}
/**
* 验证用户输入的验证码和session中存的是否一致
* @param request
*/
private void validateCode(HttpServletRequest request) {
}
}
此处需注意,StringUtils导包需要添加依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.0</version>
</dependency>
b.在配置文件中的添加配置信息
@Autowired
private ValidateCodefilter validateCodefilter;
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
//将校验码验证放在UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(validateCodefilter,UsernamePasswordAuthenticationFilter.class)
.formLogin() // 定义当需要用户登录时候,转到的登录页面。
.and()
.authorizeRequests() // 定义哪些URL需要被保护、哪些不需要被保护
.anyRequest() // 任何请求,登录后可以访问
.authenticated();
}
4.2短信验证码
短信验证码的功能实现,其实和图形验证码的原理是一样的。只不过一个是返回给前端一个图片,一个是给用户发送短消息,这里只需要去调用一下短信服务商的接口就好了。
但需要注意的是,在使用帐号密码登录的时候,UsernamePasswordAuthenticationToken里面包含了用户的帐号,密码,以及其他的是否可用等状态信息。我们是通过手机短信来做登录,所以就没有密码了,我们需要将验证流程中密码相关部分都进行修改,也即我们需要自定义认证过程中的一些类,包含
- AuthenticationToken(流程默认为UsernamePasswordAuthenticationToken),
- AuthenticationFilter(流程默认为UsernamePasswordAuthenticationFilter),
- Provider(默认DaoAuthenticationProvider),
然后将自定义的相关类配置在configure方法中。
可参考:https://blog.csdn.net/u013435893/article/details/79684027
4.3 集成 Kaptcha 实现前后端分离验证码功能
4.4第三方登录
参考https://blog.csdn.net/u013435893/article/details/79735097
相关参考:
https://blog.csdn.net/yuanlaijike/article/category/9283872
https://blog.csdn.net/u013435893/article/category/7516861
三、Shiro
Shiro是Apache下的一个开源项目,我们称之为Apache Shiro。它是一个很易用与Java项目的的安全框架,提供了认证、授权、加密、会话管理,与spring Security 一样都是做一个权限的安全框架,但是与Spring Security 相比,在于 Shiro 使用了比较简单易懂易于使用的授权方式。shiro属于轻量级框架,相对于security简单的多,也没有security那么复杂。