基本思路
服务端通过 JSON字符串,告诉前端用户是否登录、认证;前端根据这些提示跳转对应的登录页、认证页。
实现代码
添加依赖:
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--thymeleaf视图解析器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--验证码的实现依赖 https://mvnrepository.com/artifact/com.github.penggle/kaptcha -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
具体实现
AuthenticationEntryPoint:未登录
public class AjaxAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(401);
httpServletResponse.getWriter().write(Objects.requireNonNull(JsonUtils.obiectToJson(authError())));
}
}
AuthenticationFailureHandler:登录失败
public class AjaxAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletRequest.setCharacterEncoding("UTF-8");
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json;charset=UTF-8");
// 获取的是验证码的错误的消息提醒
String errmsg = (String) httpServletRequest.getAttribute("errmsg");
if (StringUtils.isNotEmpty(errmsg)){
httpServletResponse.getWriter().write(Objects.requireNonNull(JsonUtils.obiectToJson(error400(errmsg))));
}else {
httpServletResponse.getWriter().write(Objects.requireNonNull(JsonUtils.obiectToJson(error400("用户登录失败!"))));
}
}
}
AuthenticationSuccessHandler :登录成功
public class AjaxAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().
write(Objects.requireNonNull(JsonUtils.obiectToJson(success("登录成功!"))));
}
}
AccessDeniedHandler:无权访问
public class AjaxAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(403);
httpServletResponse.getWriter().write(Objects.requireNonNull(JsonUtils.obiectToJson(new AjaxResult<>(ResultCode.AUTH_ERROR))));
httpServletResponse.getWriter().flush();
}
}
LogoutSuccessHandler:注销
public class AjaxLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json;charset=UTF-8");
SecurityContextHolder.clearContext();
httpServletResponse.getWriter().write(Objects.requireNonNull(JsonUtils.obiectToJson(new AjaxResult<>(ResultCode.LOG_OUT))));
}
}
UserDetailsService:用户权限认证
/**
* @author sxyuser
*/
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserDao userDao;
@Resource
HttpServletRequest request;
@Resource
private RoleDao roleDao;
@Resource
private PermissionMapper permissionMapper;
/**
* 根据用户名查询User对象,并装配角色,权限等信息
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users loginInfo = userService.queryByUser(username);
// 给用户设置角色权限信息
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
if (ObjectUtils.allNotNull(loginInfo)){
Set<String> permsByUserId = permissionService.selectPermsByUserId(loginInfo.getId());
List<Role> roleList = roleService.selectAssignedRole(loginInfo.getId());
List<String> rList = new ArrayList<>();
// 循环得到角色信息
for (Role perm : roleList) {
if (perm != null) {
rList.addAll(Arrays.asList(perm.getRoleKey().trim().split(",")));
}
}
// 将循环得到的角色信息存入授权的权限中
for (String roleKey : rList) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_" + roleKey);
grantedAuthorities.add(grantedAuthority);
}
// 将循环得到的授权信息存入授权的权限中
for (String perm : permsByUserId) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(perm);
grantedAuthorities.add(grantedAuthority);
}
// 将登录的信息保存到session中
HttpSession session = request.getSession();
session.setAttribute("loginInfo", loginInfo);
System.out.println("grantedAuthorities:" + grantedAuthorities);
return new User(loginInfo.getUsername(), loginInfo.getPassword(), grantedAuthorities);
}else {
throw new BadCredentialsException("用户名不存在!");
}
}
}
dao层
/**
* 通过用户名密码查询单条数据
*
* @return 实例对象
*/
Users queryByUser(@Param("userName") String userName);
/**
* 根据用户ID查询权限
*
* @param id 用户ID
* @return 权限列表
*/
List<String> queryPermsByUserId(Integer id);
sql语句
<!--登录功能-->
<select id="queryByUser" resultMap="UserMap">
select
*
from users
where userName = #{userName}
</select>
<!--查询权限-->
<select id="queryPermsByUserId" resultType="string" parameterType="integer">
SELECT distinct pr.perms
FROM permission pr
LEFT JOIN role_permission rp ON pr.id = rp.permission_id
LEFT JOIN role r ON rp.role_id = r.role_id
LEFT JOIN user_role ur ON r.role_id = ur.role_id
LEFT JOIN users us ON us.id = ur.user_id
WHERE us.id = #{id}
</select>
AuthenticationProvider:前端交互
public class SelfAuthenticationProvider implements AuthenticationProvider {
@Resource
UserDetailsServiceImpl userDetailsService;
@Resource
HttpServletRequest request;
@Resource
HttpServletResponse response;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String userName = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
UserDetails userInfo = userDetailsService.loadUserByUsername(userName);
if (!EncryptPassword.decryptPwd(password, userInfo.getPassword())) {
throw new BadCredentialsException("用户名密码不正确,请重新登陆!");
}
//判断提交地址和提交方式来验证验证码的正误
if ("/doLogin".equals(request.getServletPath()) && "post".equalsIgnoreCase(request.getMethod())) {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
//获取表单提交的验证码的值
String verification = request.getParameter("code");
HttpSession session = request.getSession();
//获取下发的存在session中的验证码的值
String captcha = (String) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
if (captcha == null) {
request.setAttribute("errmsg", "验证码不为空!");
throw new BadCredentialsException("验证码不为空!");
} else if (!captcha.equals(verification)) {
request.setAttribute("errmsg", "验证码不匹配!");
throw new BadCredentialsException("验证码不匹配!");
} else if (StringUtils.isBlank(captcha)) {
session.removeAttribute(Constants.KAPTCHA_SESSION_KEY);
request.setAttribute("errmsg", "验证码已过期,请重新获取!");
throw new BadCredentialsException("验证码已过期,请重新获取!");
}
}
return new UsernamePasswordAuthenticationToken(userName, password, userInfo.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return true;
}
}
密码加密类
public class EncryptPassword {
/**
* SpringSecurity加密
*
* @param password 密码
* @return 加密后的密码
*/
public static String encryptionPwd(CharSequence password) {
return new BCryptPasswordEncoder().encode(password);
}
/**
* 加密密码和输入密码的对比
* @param rawPassword 输入的原密码
* @param encodedPassword 加密后的密码
* @return
*/
public static Boolean decryptPwd(CharSequence rawPassword, String encodedPassword){
return new BCryptPasswordEncoder().matches(rawPassword, encodedPassword);
}
}
WebSecurityConfigurerAdapter:登录拦截全局配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity security) throws Exception {
security.cors().and()
// .addFilterBefore(loginAuthenticationFilters(),UsernamePasswordAuthenticationFilter.class)
//对请求进行授权
.authorizeRequests()
.antMatchers("/").permitAll()
// 注册地址
.antMatchers("/path/toPage/reg").permitAll()
// 验证码地址
.antMatchers("/captcha/captchaImage").permitAll()
//任何请求
.anyRequest()
//需要登录以后才可以访问
.authenticated()
.and()
.formLogin()
.passwordParameter("password")
.usernameParameter("username")
.loginPage("/login")
.loginProcessingUrl("/doLogin")
// 登录成功
.successHandler(ajaxAuthenticationSuccessHandler())
// 登录失败
.failureHandler(authenticationFailureHandler())
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(ajaxLogoutSuccessHandler())
.permitAll()
.and().csrf().disable()
.httpBasic();
// // 基于Token不需要session
// security.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
security.exceptionHandling()
// .authenticationEntryPoint(authenticationEntryPoint())
.accessDeniedHandler(accessDeniedHandler());
// 禁用缓存
security.headers().cacheControl();
}
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.authenticationProvider(selfAuthenticationProvider());
}
/**
* 解决静态资源被拦截的问题
*
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/doc.html", "/fonts/**",
"/img/**", "/swagger-ui.html", "/v2/**", "/swagger-resources/**",
"/jquery/**", "/js/**", "/layer/**",
"/script/**", "/ztree/**",
"/css/**", "/bootstrap/**", "/webjars/**","/favicon.ico");
}
/**
* 未登陆时返回 JSON 格式的数据给前端(否则为 html)
*
* @return
*/
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new AjaxAuthenticationEntryPoint();
}
/**
* 登录成功返回的 JSON 格式数据给前端(否则为 html)
*
* @return
*/
@Bean
public AjaxAuthenticationSuccessHandler ajaxAuthenticationSuccessHandler() {
return new AjaxAuthenticationSuccessHandler();
}
/**
* 登录失败返回的 JSON 格式数据给前端(否则为 html)
*
* @return
*/
@Bean
public AjaxAuthenticationFailureHandler authenticationFailureHandler() {
return new AjaxAuthenticationFailureHandler();
}
/**
* 注销成功返回的 JSON 格式数据给前端(否则为 登录时的 html)
*
* @return
*/
@Bean
public AjaxLogoutSuccessHandler ajaxLogoutSuccessHandler() {
return new AjaxLogoutSuccessHandler();
}
/**
* 无权访问返回的 JSON 格式数据给前端(否则为 403 html 页面)
*
* @return
*/
@Bean
public AjaxAccessDeniedHandler accessDeniedHandler() {
return new AjaxAccessDeniedHandler();
}
/**
* 自定义安全认证
*
* @return
*/
@Bean
public SelfAuthenticationProvider selfAuthenticationProvider() {
return new SelfAuthenticationProvider();
}
}
前端页面:用户名和密码的name属性需要为username,password,如果不使用这两个可以在config文件中进行修改
.passwordParameter("password")
.usernameParameter("username")
<form class="form-signin" role="form" id="signupForm" autocomplete="off">
<h2 class="form-signin-heading"><i class="glyphicon glyphicon-log-in"></i> 用户登录</h2>
<div class="form-group has-success has-feedback">
<input type="text" class="form-control" id="username" name="username" placeholder="请输入登录账号" autofocus>
<span class="glyphicon glyphicon-users form-control-feedback"></span>
</div>
<div class="form-group has-success has-feedback">
<input type="password" class="form-control" id="password" name="password" placeholder="请输入登录密码" style="margin-top:10px;">
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
</div>
<div class="form-group has-success has-feedback">
<input type="text" class="form-control" id="code" name="code" placeholder="请输入验证码" style="margin-top:10px;margin-bottom: 10px;">
<img th:src="@{/captcha/captchaImage(type='math')}" class="imgcode" width="100%" alt="验证码"/>
</div>
<div class="checkbox" style="text-align:right;"><a th:href="@{/path/toPage/reg}" href="reg.html">我要注册</a></div>
<button class="btn btn-lg btn-success btn-block" type="submit" id="btnSubmit"> 登录 </button>
</form>
<script th:inline="javascript"> var ctx = [[@{/}]];</script>
js的写法,需要引入表单验证的js,也可以使用其他方式的表单提交方式,需要注意的是ajax中的url地址需要和config中的loginProcessingUrl("/doLogin")的地址保持相同。
$(function() {
validateRule();
//刷新切换验证码
$('.imgcode').click(function () {
var url = ctx + "captcha/captchaImage?type=math&s=" + Math.random();
$(".imgcode").attr("src", url);
});
});
//submitHandler 当表度单通过验证,提交表单
function login() {
let serverMsg = '';
let username = $("#username").val();
let password = $("#password").val();
let code = $("#code").val();
$.ajax({
type: "post",
url: ctx + "doLogin",
data: {
"username": username,
"password": password,
"code": code
},
dataType: "json",
async: false,
xhrFields: {
withCredentials: true
},
success: function(result) {
console.log("输出结果:", result.data);
console.log("输出提示:", result.msg);
console.log("输出错误", errmsg);
serverMsg = result.data;
if (result.code === 200) {
layer.msg(serverMsg, {
icon: 1,
time: 3000
}, function () {
location.href = ctx + "user/main";
});
} else {
layer.msg(serverMsg, {
icon: 2,
time: 3000
},function () {
//清除表单内容
$("#signupForm").clearForm();
});
}
},
error:function (error) {
console.log("错误信息",this.error);
}
});
}
function validateRule() {
var icon = "<i class='fa fa-times-circle'></i> ";
$("#signupForm").validate({
rules: {
username: {
required: true
},
password: {
required: true
},
code: {
required: true
}
},
messages: {
username: {
required: icon + "用户名不能为空!",
},
password: {
required: icon + "密码不能为空!",
},
code: {
required: icon + "验证码不能为空!"
}
},
submitHandler: function() {
login();
}
})
}
验证码的后台实现
验证码的配置
/**
* 验证码配置
*
* @author sxy
*/
@Configuration
public class CaptchaConfig {
@Bean(name = "captchaProducer")
public DefaultKaptcha getKaptchaBean() {
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// 是否有边框 默认为true 我们可以自己设置yes,no
properties.setProperty(KAPTCHA_BORDER, "yes");
// 验证码文本字符颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
// 验证码图片宽度 默认为200
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
// 验证码图片高度 默认为50
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
// 验证码文本字符大小 默认为40
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
// KAPTCHA_SESSION_KEY
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
// 验证码文本字符长度 默认为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,Courier");
// 图片样式 水纹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.ShadowGimpy");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
@Bean(name = "captchaProducerMath")
public DefaultKaptcha getKaptchaBeanMath() {
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// 是否有边框 默认为true 我们可以自己设置yes,no
properties.setProperty(KAPTCHA_BORDER, "yes");
// 边框颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");
// 验证码文本字符颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
// 验证码图片宽度 默认为200
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
// 验证码图片高度 默认为50
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
// 验证码文本字符大小 默认为40
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");
// KAPTCHA_SESSION_KEY
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
// 验证码文本字符间距 默认为2
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
// 验证码文本字符长度 默认为5
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
// 验证码文本生成器
properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.demo.springboot.config.KaptchaTextCreator");
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
// 验证码噪点颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
// 干扰实现类
properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
// 图片样式 水纹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.ShadowGimpy");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
注意里面的KAPTCHA_TEXTPRODUCER_IMPL属性,这个对应自己项目的本地路径。
验证码文本生成器
/**
* 验证码文本生成器
*
* @author sxy
*/
public class KaptchaTextCreator extends DefaultTextCreator {
private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(",");
@Override
public String getText() {
int result = 0;
Random random = new SecureRandom();
int x = random.nextInt(10);
int y = random.nextInt(10);
StringBuilder suChinese = new StringBuilder();
int randomoperands = (int) Math.round(Math.random() * 2);
if (randomoperands == 0) {
result = x * y;
suChinese.append(CNUMBERS[x]);
suChinese.append("*");
suChinese.append(CNUMBERS[y]);
} else if (randomoperands == 1) {
if (!(x == 0) && y % x == 0) {
result = y / x;
suChinese.append(CNUMBERS[y]);
suChinese.append("/");
suChinese.append(CNUMBERS[x]);
} else {
result = x + y;
suChinese.append(CNUMBERS[x]);
suChinese.append("+");
suChinese.append(CNUMBERS[y]);
}
} else if (randomoperands == 2) {
if (x >= y) {
result = x - y;
suChinese.append(CNUMBERS[x]);
suChinese.append("-");
suChinese.append(CNUMBERS[y]);
} else {
result = y - x;
suChinese.append(CNUMBERS[y]);
suChinese.append("-");
suChinese.append(CNUMBERS[x]);
}
} else {
result = x + y;
suChinese.append(CNUMBERS[x]);
suChinese.append("+");
suChinese.append(CNUMBERS[y]);
}
suChinese.append("=?@").append(result);
return suChinese.toString();
}
}
验证码的controller
/**
* 图片验证码(支持算术形式)
*
* @author sxy
*/
@Controller
@RequestMapping("/captcha")
public class CaptchaController{
@Resource(name = "captchaProducer")
private Producer captchaProducer;
@Resource(name = "captchaProducerMath")
private Producer captchaProducerMath;
/**
* 验证码生成
*/
@GetMapping(value = "/captchaImage")
public ModelAndView getKaptchaImage(HttpServletRequest request, HttpServletResponse response) {
ServletOutputStream out = null;
try {
HttpSession session = request.getSession();
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.addHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
response.setContentType("image/jpeg");
String type = request.getParameter("type");
String capStr;
String code = null;
BufferedImage bi = null;
if ("math".equals(type)) {
String capText = captchaProducerMath.createText();
capStr = capText.substring(0, capText.lastIndexOf("@"));
code = capText.substring(capText.lastIndexOf("@") + 1);
bi = captchaProducerMath.createImage(capStr);
} else if ("char".equals(type)) {
capStr = code = captchaProducer.createText();
bi = captchaProducer.createImage(capStr);
}
session.setAttribute(Constants.KAPTCHA_SESSION_KEY, code);
out = response.getOutputStream();
assert bi != null;
ImageIO.write(bi, "jpg", out);
out.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
注意:在启动类上一定不要加@ComponentScan注解,加了之后不知道什么原因,controller层的映射地址全访问不到,具体原因不知。
参考文章
https://www.jianshu.com/p/9d841e055efb
https://ruoyi.vip/